a04ecf7a1927b95405dcd406847145c3f7d3075c
- Voice messages: MediaRecorder → encrypted attachment, custom waveform player via OfflineAudioContext, 60s limit + live mic-level meter - Offline message queue: localStorage outbox, exponential backoff retries, optimistic pending bubble with retry/discard - Delivery indicator: message_deliveries table + RLS (reciprocal receipts), ✓ / ✓✓ / ✓✓-blue tick states, group-aware (all members must ack) - Per-participant volume slider in calls via right-click tile menu, persisted to localStorage, applied to attached audio elements - Group call scaling: grid up to 12 tiles with pagination, active-speaker auto-promotion in fullscreen - Push notifications scaffolding: service worker, VAPID subscription registration, notify-push edge function skeleton - Backup recovery code: 24-char base32 code (~120 bits entropy) as alternative decrypt path, restore UI with mode toggle - Admin panel: conversations list, audit log (admin_audit_log table + admin_log_action RPC), audit entry on user flag toggle - Search v2: sender filter, attachment-only toggle, date range - Reactions pop animation (scale 0.4→1.15→1 on count change) - Message list windowing (150 default, expand via IntersectionObserver) - Stub cleanup: removed dead ScreenshareStub from CallParticipantTile Fixes: - Focus-triggered flicker: dropped window.focus listeners in three spots, throttled visibilitychange/online wake-refreshes to 30s, keep existing data visible during background re-syncs (no more spinner on every click) - Voice attachment audio element collapsed to 0px on peer side — now forces 280px min-width on bubble Migrations (push required): 20260421000001_message_deliveries.sql 20260421000002_admin_audit_log.sql Server TODO: VAPID keys + notify-push edge function deploy
chat-app
Private, end-to-end encrypted chat app for a small circle. Self-hosted Supabase backend, clients on iOS, Android, macOS, Windows, and Linux.
Server is zero-knowledge: it only stores ciphertexts and metadata needed to route them.
Milestone Roadmap
- Milestone 1 — Text Chat (current): auth, identity keys, 1:1 encrypted messaging, local history, push.
- Milestone 2 — Voice Calls: libsodium-secured WebRTC signaling via Supabase Realtime.
- Milestone 3 — Video Calls: same stack, add video tracks + bandwidth handling.
- Milestone 4 — Desktop Polish: feature parity with mobile, tray, notifications.
- Milestone 5 — Groups & Channels: Discord-like group channels with shared ratchet keys.
Repository Layout
apps/
mobile/ Expo + React Native (iOS / Android)
desktop/ Tauri v2 + React + Vite + Tailwind (macOS / Windows / Linux)
packages/
shared/ Business logic: Supabase client, libsodium crypto, auth, chat
db-types/ Generated Supabase database types
ui-web/ React web components shared by desktop (not by React Native)
infra/
supabase/ Self-hosting docs, client env, SQL migrations
.github/workflows/ CI + build placeholders
Tech Stack
- Mobile: Expo SDK 52, Expo Router, expo-secure-store, expo-sqlite, expo-notifications, react-native-libsodium.
- Desktop: Tauri v2, React 18, Vite, Tailwind, tauri-plugin-stronghold, tauri-plugin-sql, tauri-plugin-notification, Zustand.
- Backend: Supabase self-hosted (Postgres + GoTrue + PostgREST + Realtime + Storage + Edge Functions) on Hetzner, Caddy reverse proxy.
- Crypto: X25519 identity keys, XChaCha20-Poly1305 envelopes (libsodium / NaCl).
- Auth: email magic-link, invite-only.
Prerequisites
- Node 22+ (see
.nvmrc) - pnpm 9+
- Rust + Cargo (Tauri): https://rustup.rs
- Docker + Docker Compose (for running Supabase locally or on the VPS)
- Platform toolchain per target:
- iOS: Xcode
- Android: Android Studio + SDK
- macOS/Linux/Windows Tauri: see
apps/desktop/README.md
Setup
# 1. Node + pnpm (macOS)
brew install pnpm
corepack enable
# 2. Clone + install
git clone <this repo>
cd chat-app
pnpm install
# 3. Env
cp infra/supabase/.env.example apps/mobile/.env
cp infra/supabase/.env.example apps/desktop/.env
# Fill in SUPABASE_URL and SUPABASE_ANON_KEY from your self-hosted stack.
See infra/supabase/README.md for spinning up the backend.
Common Scripts (run from repo root)
| Script | Purpose |
|---|---|
pnpm dev |
Start every workspace's dev task (Turborepo). |
pnpm build |
Build every workspace. |
pnpm lint |
ESLint across workspaces. |
pnpm typecheck |
TypeScript project-wide type check. |
pnpm test |
Vitest across workspaces. |
pnpm format |
Prettier write. |
pnpm format:check |
Prettier check. |
pnpm mobile:dev |
expo start for the mobile app. |
pnpm mobile:ios |
Native iOS run. |
pnpm mobile:android |
Native Android run. |
pnpm desktop:dev |
tauri dev for the desktop app. |
pnpm desktop:build |
Platform-specific Tauri bundle. |
pnpm db:types |
Regenerate @chat-app/db-types from the running Supabase. |
Architecture Overview
- Shared-first: Anything that can run in both React Native and the Tauri WebView lives in
packages/sharedand is imported via@chat-app/shared/*. Both hosts pass in adapters for platform-only concerns (secure storage, SQLite, libsodium backend). - Zero-knowledge server: Messages are encrypted client-side before insert. The server sees ciphertexts, a conversation id, a sender id, and a timestamp — nothing else.
- Key custody: X25519 private keys live in platform secure stores only (Keychain/Keystore on mobile, Stronghold on desktop). Public keys live in the
profilestable. - Realtime: Supabase Realtime delivers new ciphertext rows to subscribed clients. Push notifications are silent (data-only) — the client decrypts and composes the visible notification locally.
Security Checklist
- RLS enabled on every user-facing table
- Invite-only enforced in SQL (invites table + policy)
- No plaintext in push payloads
- Service role key never shipped to a client
- JWT secret rotated on first boot
- TLS via Caddy at the edge
Description
Languages
TypeScript
76.3%
Makefile
9.5%
JavaScript
4.3%
PLpgSQL
4%
CSS
3.3%
Other
2.6%