Private conversations, made simple. A privacy-first E2EE web messenger prototype (repo SecureTalks) with server-blind encrypted storage, identity verification, encrypted attachments, disappearing messages, and opt-in AI tools.
Solo maintainer · research & architecture · @kulharshit21
Privyra / SecureTalks is a privacy-first encrypted messaging prototype: plaintext stays in an unlocked browser session; Supabase holds ciphertext, AEAD metadata, and encrypted Storage blobs. Attachments (encrypt-then-upload) use the private encrypted-attachments bucket for direct chats — group attach remains incomplete in UI. Disappearing messages combine expires_at, RLS, purge_expired_messages(), and cleanup_expired_messages(). Group chat is a small-team MVP (symmetric epochs — not MLS). Opt-in AI runs only via mistral-ai-assist Edge Function (Next proxy disabled by default); normal send/decrypt never calls AI.
Docs: SECURITY_CLAIMS.md · LIMITATIONS.md · DEPLOYMENT.md · DEMO_SCRIPT.md · docs/THREAT_MODEL.md · docs/SECURITY_MODEL.md · docs/PRODUCTION_ENV_CHECKLIST.md · docs/CRON_CLEANUP.md · docs/privyra-graph-summary.json (curated agent / architecture graph)
⚠️ Timers and server deletion do not stop screenshots, malware, or a compromised device. This project does not claim to be “more secure than WhatsApp” without an independent audit — see LIMITATIONS.md.
Remote: kulharshit21/SecureTalks — description, topics (e.g. privyra, e2ee, supabase, libsodium), and releases are set there.
flowchart TB
subgraph Browser["🔒 Trust boundary — your tab"]
direction TB
UI["Next.js App Router UI<br/>shadcn · Tailwind"]
Vault["Device gate · PIN unlock<br/>private keys in IndexedDB (idb)"]
Cipher["SessionCipher<br/>X25519 triple DH · XChaCha AEAD envelope"]
FileAEAD["File symmetric AEAD<br/>random file key · nonce ‖ ciphertext"]
UI --> Vault
Vault --> Cipher
Cipher --> FileAEAD
end
subgraph Edge["Supabase — hostile infra assumed"]
direction TB
Auth["GoTrue Auth"]
PG["Postgres + RLS<br/>messages · members · attachments meta"]
ST["Private bucket<br/>attachments (ciphertext only)"]
RT["Realtime<br/>typing · inserts"]
end
Browser -->|"JWT"| Auth
Cipher -->|"insert ciphertext row"| PG
FileAEAD -->|"XHR upload ciphertext"| ST
PG <-->|"membership-scoped SELECT"| Browser
ST <-->|"policy via attachments row"| Browser
PG --> RT
sequenceDiagram
participant U as User
participant C as Client crypto
participant DB as Postgres messages
participant S as Storage bucket
U->>C: Plaintext / file bytes
C->>C: derive outbound DH secrets<br/>wrap UTF8 manifest OR body in AEAD
C->>DB: ciphertext + nonce + algorithm<br/>associated_data JSON (devices + conversation + ts)
opt Attachment
C->>C: random fileKey · AEAD(file)<br/>wrap fileKey with SessionCipher
C->>S: POST ciphertext blob only
C->>DB: attachments row (wrapped key + path + mime hint)
end
| Surface | What the server sees | What it never sees |
|---|---|---|
messages |
Ciphertext, nonce, protocol id, bound metadata JSON | Plaintext bodies |
attachments |
Storage path, wrapped file key blob (still ciphertext to infra without device secrets), MIME hint | Plain files |
| Client compromise | N/A — attacker reads decrypted UX same as user | — |
Read docs/SECURITY_RLS_CHECKLIST.md after migrating. SECURITY_CLAIMS.md records what we do and do not promise.
| Layer | Choices |
|---|---|
| App | Next.js 16 · React 19 · Tailwind 4 · shadcn/ui patterns · lucide-react |
| Auth | Supabase Auth (+ SSR @supabase/ssr) |
| Crypto | libsodium-wrappers · session envelope + separate file AEAD domain |
| Data | Postgres RLS · Storage policies · optional pg cron purge |
git clone https://github.com/kulharshit21/SecureTalks.git
cd SecureTalks
npm ci
cp .env.example .env.local # fill NEXT_PUBLIC_SUPABASE_* + redirect URLs
npm run devApply SQL from supabase/migrations/ via Supabase CLI or SQL editor (order matters).
| Command | Purpose |
|---|---|
npm run dev |
Local Next dev server |
npm run build |
Production build |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
npm run test |
Vitest unit/crypto tests |
npm run test:e2e |
Playwright (configure env first) |
npm run security:edge-health |
Probe Edge Functions (cleanup + AI); needs .env.local with anon URL/key |
npm run smoke:two-user-chat |
Optional multi-user smoke script (configure test accounts first) |
Auth / device recovery (UI): /forgot-password, /reset-password, /recover-device, /settings/security — see docs/SECURITY_MODEL.md.
After migrations, public.purge_expired_messages() deletes expired attachment objects then rows. Schedule with Supabase pg_cron or a trusted worker using service_role (already granted EXECUTE in migration). Never ship the service role key to the browser.
Author & sole contributor: kulharshit21
Built with intent · vibe-coded UI · honest crypto boundaries