Messenger/claude.md
Заид Омар Медхат | Zaid Omar Medhat 62f20f8843 init
2026-07-10 13:31:12 +05:00

192 lines
9.8 KiB
Markdown

# Project Spec — Realtime Chat & Calls App
> This document is a brief for Claude Code (plan mode). Read it fully, then propose a
> plan before writing any code. Build the skeleton first, then implement part by part
> with check-ins. Do NOT scaffold the whole thing in one pass. See "How to work" at the
> bottom — it is as important as the architecture.
---
## 1. What we are building
A self-hosted, cross-platform messaging app with:
- **1:1 direct messages and group messages** (Telegram-shaped).
- **Media messages**: voice notes, video messages, images, videos (files).
- **Voice/video calls with screen share** — group conferences and 1:1. **This is a LATER
phase. Do not scaffold call/WebRTC code until the chat core is working and I say so.**
Targets: web, iOS, Android, and desktop (macOS + Windows).
## 2. Core mental model (read this before anything else)
The realtime transport (**Centrifugo**) is a **dumb, fast pipe**, NOT the chat server.
The **backend is the brain**. The message-send flow is deliberately:
1. Client POSTs "send message" to the backend REST API (the client's realtime socket is
effectively **receive-only**).
2. Backend authorizes, writes the message to Postgres (source of truth), then calls
Centrifugo's HTTP API to publish into the channel.
3. Centrifugo fans it out to subscribers.
Do not let clients publish directly through Centrifugo. All business logic, validation,
and persistence live in the backend.
### Channel model
- `room:<roomId>` — one channel per room; all members subscribe.
- `user:<userId>` — one **personal channel** per user. DMs are delivered by publishing the
message into BOTH participants' personal channels; the client routes by `conversationId`
in the payload. This scales to one subscription per user regardless of conversation count,
and doubles as the notification / unread / "added to room" channel.
### Two auth gates (keep them separate)
- **Connection auth**: backend mints a JWT (shared secret with Centrifugo) at login;
Centrifugo verifies the signature — no backend call per connection.
- **Subscription auth**: use the **subscribe-proxy** pattern — Centrifugo calls a backend
endpoint on each subscribe to check Postgres membership ("can user U join channel C?").
This reflects current DB state (kicked users lose access immediately). Personal-channel
subscriptions are authorized by matching the token's user id.
### Boundaries to respect
- Centrifugo history is a **short recovery buffer** (reconnect replay), NOT the message
store. Load conversation history from Postgres via REST.
- **Server-assigned message IDs + ordering** and **client-side dedupe** are required from
day one (messages can arrive both live and via history load). These are painful to
retrofit.
- Media (voice notes / video messages / images) upload to object storage (MinIO) via the
backend (presigned URLs); only a **reference** (URL + metadata) is published through the
channel. Never push binary through Centrifugo.
## 3. Stack (decided — do not re-litigate)
- **Language everywhere:** TypeScript.
- **Monorepo:** single repo, pnpm workspaces. (NOT separate repos — shared code is imported
by source so type changes surface as immediate compile errors.)
- **Backend framework:** Fastify. (NestJS was considered and rejected: too much
ceremony/DI for a small team and a realtime/event-shaped workload.)
- **Validation:** JSON Schema via Fastify's built-in support, sourced from the shared package.
- **Realtime transport:** Centrifugo (self-hosted, external service).
- **DB:** Postgres 16 (source of truth). **Redis** for Centrifugo scaling + history/recovery.
- **Object storage:** MinIO (S3-compatible).
- **Web frontend:** React + TypeScript + Vite.
- **Mobile frontend:** React Native + Expo + TypeScript.
- **Desktop:** thin Tauri (preferred) or Electron shell wrapping the web build.
- **Calls (LATER phase):** LiveKit (SFU) + coturn (TURN over TLS/443). Not now.
## 4. Monorepo layout
```
/
├── package.json # pnpm workspace root
├── pnpm-workspace.yaml
├── tsconfig.base.json
├── packages/
│ ├── core/ # THE KEYSTONE — platform-agnostic, imported by all
│ │ ├── src/
│ │ │ ├── types/ # message/event/API payload types
│ │ │ ├── schemas/ # JSON Schemas (backend validates, clients type off these)
│ │ │ ├── channels/ # channel-name builders: room:<id>, user:<id>
│ │ │ ├── realtime/ # Centrifugo SDK wrapper (UI-agnostic)
│ │ │ ├── api/ # typed API client functions
│ │ │ └── messages/ # dedupe + ordering logic
│ │ └── package.json
│ ├── backend/ # Fastify service; imports core
│ │ ├── src/
│ │ │ ├── routes/ # auth, messages, rooms, conversations, media, subscribe-proxy
│ │ │ ├── plugins/ # db, redis, centrifugo client, auth/jwt
│ │ │ ├── db/ # migrations + queries
│ │ │ └── server.ts
│ │ └── package.json
│ ├── web/ # React + Vite; imports core
│ │ └── package.json
│ ├── mobile/ # React Native + Expo; imports core
│ │ └── package.json
│ └── desktop/ # Tauri/Electron shell over web build (config-heavy)
│ └── package.json
├── infra/ # docker-compose for Centrifugo, Postgres, Redis, MinIO
│ ├── docker-compose.yml
│ └── centrifugo/config.json
└── PROJECT_SPEC.md
```
### Rules for `core`
- No `react-dom` or `react-native` imports anywhere in `core`. It is pure TS.
- Keep React hooks / state logic UI-agnostic where possible so it can migrate into `core`
later. Do NOT bake DOM/RN assumptions into the Centrifugo/API wrappers.
- One source of truth for payload shapes: types + JSON Schemas live here, backend and
clients both consume them.
## 5. Data model (starting point — refine in planning)
Tables (Postgres): `users`, `rooms`, `room_members`, `conversations` (for DMs),
`messages`, `read_state` (per-user, per-conversation last-read message id).
- `messages` needs a **server-assigned monotonic id/sequence** (e.g. BIGSERIAL or per-room
counter) and a stable id used for client-side dedupe.
- Membership drives channel access (the subscribe-proxy checks these tables).
## 6. Build phases (implement in this order)
**Phase 0 — Skeleton.** Monorepo tooling, workspaces, tsconfig, empty packages that build
and import `core`. `infra/docker-compose.yml` bringing up Centrifugo + Postgres + Redis +
MinIO locally. No features yet. Verify everything boots.
**Phase 1 — Auth + connection.** User signup/login in backend; JWT minting for Centrifugo;
web client connects its (receive-only) socket and subscribes to its personal channel via
the subscribe-proxy. Prove end-to-end connectivity.
**Phase 2 — Rooms + messaging.** Room CRUD, membership, subscribe-proxy authorization,
send-message REST endpoint → persist → publish to `room:<id>`. History load from Postgres.
Server-assigned ids + client dedupe. Web client sends/receives in a room.
**Phase 3 — DMs.** Personal-channel delivery (publish to both participants), conversation
model, client routing by `conversationId`.
**Phase 4 — Media messages.** MinIO presigned uploads; voice notes / video messages /
images as references published through channels.
**Phase 5 — Mobile client.** React Native + Expo consuming the same `core`. Reuse realtime
- API layer; platform-specific UI only.
**Phase 6 — Desktop shell.** Tauri/Electron over the web build.
**Phase 7 (LATER, separate effort) — Calls.** LiveKit + coturn, call signaling over
Centrifugo (ring via personal channel), token minting in backend, screen share. Do NOT
start until explicitly told; video across platforms is the hardest part of the project and
gets its own focused phase.
## 7. Scaling assumptions (design for, don't build yet)
- Backend and Centrifugo are **stateless** — no in-memory session state; everything in
Postgres/Redis. This is what lets them scale horizontally later.
- Media plane (LiveKit/coturn) stays **physically separate** from the control plane.
- Redis is present from Phase 0 (coordination layer for later Centrifugo + LiveKit clustering).
## 8. How to work (IMPORTANT — follow this)
- **Plan first.** Before writing code, produce a plan for the current phase only and wait
for my approval. Do not jump ahead to later phases.
- **Explain before implementing.** For each phase, briefly explain the approach and the
reasoning behind key decisions before generating code. I prefer understanding the "why".
- **Go part by part with check-ins.** Implement one coherent piece, stop, let me review,
then continue. Do not dump large amounts of generated code in one go.
- **Do not add features I didn't ask for.** No calls/WebRTC before Phase 7. No speculative
abstractions.
- **Ask when a decision is ambiguous** rather than guessing, especially on the data model
and API shapes.
- **Respect the decided stack** in section 3 — don't substitute frameworks or add heavy
dependencies without flagging why.
- Keep the shared `core` package clean of platform-specific imports.
## 9. Legal / deployment note (context, not a task)
Deployment target includes the Russian Federation. There are messenger-registration and
data-localization rules that carry real liability; this is for the humans to handle with
local legal advice — not something to implement or work around in code. Network reliability
on poor connections (TURN-over-443, simulcast) is legitimate engineering and belongs in the
LATER calls phase, but is not a censorship-circumvention feature and should not be built as one.