# Phase 2 — status

ARCHITECTURE.md §8 Phase 2 scope is large ("the phase that decides the
product," 7-9 weeks indicative). This records what's actually built and
verified in this session versus what's still open, honestly — matching
SPIKES.md's standard for Phase 1.

## Built and verified live (real Postgres, real browser, real HTTP)

- **Channels**: create/list/join/leave/archive/rename, 1:1 DMs (deduped),
  group DMs (Features 3.1–3.6, 3.9 minus star/mute/hide).
- **Messages**: send (idempotent via `UNIQUE(channel_id, client_msg_id)`,
  I3), edit (revision-gated, I8), soft-delete (content scrubbed at every
  read path), reactions, pins, scrollback, thread replies (data model +
  API only, no thread UI yet) — Features 4.1, 4.2 (plain text only, no
  rich composer), 4.3 (data model only), 4.4, 4.6, 4.13.
- **Mentions**: `@here`/`@channel`/`@everyone` **and now real `@user`
  mentions** persist to `message_mentions` with a resolved `target_id`.
  Added `users.username` (globally unique, auto-generated from the email
  local-part at signup, backfilled for existing rows in the migration).
  Resolution only matches users who are actual members of the mentioning
  message's workspace — a handle match outside it would otherwise leak
  account existence. Verified live: sent a message with `@<username>`,
  confirmed `message_mentions` got the real user id, not a null. Per-
  workspace handles (like Slack's own) would be a bigger feature; this is
  deliberately the minimal global-username version needed for mentions
  to actually resolve.
- **Read state**: channel `last_read_seq` (GREATEST-clamped, I4) and
  per-thread `last_read_reply_seq` (ADR-014) — API only, no unread-badge UI.
- **Sync engine** (`libs/sync`): `MessageStore` (I1 total order, I3
  dedup-by-id, I8 revision guard, bounded LRU window), `Outbox` (persist-
  before-send, replay-on-reconnect), `ChannelSync` (I7 catch-up/live
  boundary with buffering, gap detection). 12 unit tests passing.
- **Web UI**: real channel list, message view, and composer at
  `/channel?workspaceId=&channelId=`, wired to the actual sync engine —
  not a hand-rolled fetch-and-render. Manually exercised in a real browser:
  send, idempotent retry, edit, delete-with-scrub, and full page-reload
  catch-up all confirmed working.
- **Realtime plumbing** (`apps/gateway`, `libs/messaging`'s events-bus):
  **now verified live end-to-end.** Redis (8.10.0, installed via `scoop
  install redis` — a native Windows port, no Docker/admin/WSL needed) is
  running locally. Confirmed with two real browser tabs: a message sent
  from tab 2 appeared in tab 1 with zero refresh, proving the full path —
  `apps/api` write → Redis publish → `apps/gateway` subscription → socket
  room broadcast → `ChannelSync.onLiveMessage` on the receiving client.
  The web UI's HTTP-only degradation (confirmed earlier when Redis was
  down) remains as the correct fallback, but the realtime path itself is
  no longer just "should work" — it's demonstrated working.

## Built and verified since Redis landed

- **Presence (§4.4)**: hand-built on Redis exactly per spec — device-count
  tracking via `SCARD` on a per-user socket set (not a separately
  incremented counter, so it can't drift), TTL-based liveness tokens, a
  30s sweeper reconciling against surviving TTLs (covers a gateway dying
  ungracefully — I9's "offline within 60s" bound), `presence:changed`
  broadcasts only on an actual 0↔1 device-count transition. An HTTP
  snapshot endpoint (`GET /workspaces/:id/presence` on `apps/gateway`)
  handles initial hydration. **Verified live**: opened a channel in one
  tab (registers presence), confirmed 🟢 in another tab's member list,
  closed the first tab, confirmed it flipped to offline within seconds.
  Single-node caveat documented in code: the sweeper is a plain
  `setInterval` in one gateway process, correct for this dev setup but
  needing a single elected job (or `apps/worker`) in a real multi-node
  deployment.
- **Typing indicators**: server-throttled to 1 event per user per channel
  per 3s via Redis `SET NX EX` (atomic, safe under a user's multiple
  tabs), broadcast through the same events-bus as everything else,
  client-side auto-clear ~4s after the last event (no explicit "stopped"
  signal exists, matching §4.3). **Verified live** across two tabs.
- **Outbox pending/failed/retry UI** (4.23): the channel page now renders
  an optimistic bubble immediately on send (before the network attempt,
  matching the outbox's own persist-first ordering), transitions it to
  "(failed to send)" with a retry button on failure, and removes it once
  the real message lands in the store. `libs/sync`'s `Outbox` gained an
  optional `onStatusChange` observer to make this possible without
  changing its core contract.
- **Proactive token refresh for sockets**: a real bug found while testing
  presence — an expired 15-minute access token gets a socket connection
  silently rejected (no HTTP-style 401-then-refresh-then-retry available
  for a socket handshake). Fixed with `getValidAccessToken()` (decodes the
  JWT's `exp` client-side, refreshes if <30s remain) used both at initial
  connect and via socket.io's `auth` callback form so *reconnects* also
  get a fresh token, not the same stale one forever.
- **Gateway CORS gap**: a second real bug — `apps/gateway`'s WebSocket
  `cors` option does not cover its plain HTTP routes (Fastify needs its
  own `@fastify/cors` registration, same as `apps/api`). The presence
  snapshot endpoint worked perfectly via curl and failed silently in the
  browser until this was added — worth remembering for any future HTTP
  route added to the gateway app.

## Not built — real scope, not oversights

- **File upload** (5.1, 5.2): needs S3/MinIO wiring (docker-compose has
  MinIO but it's untested — no Docker in this environment either) plus
  presigned-URL endpoints, none of which exist yet.
- **Rich composer** (ProseMirror, Feature 4.2): the composer is a plain
  `<input>`. `libs/editor` is an empty placeholder. This is also OD-2's
  spike (SPIKES.md) — building the real composer and measuring it against
  the performance budget are the same undone work.
- **Emoji picker, search (6.1/6.3/6.4), Typesense indexing**: not started.
- **PWA install/badge/push live-tested** (12.1, 12.3, 12.4): the manifest
  and service worker exist from Phase 0 but haven't been verified against
  an actual installed PWA or a live push subscription.
- **The verification suite itself** (§6/ADR-011 — property-based tests via
  fast-check, the seeded fault-injection harness, the TLA+ model): this is
  explicitly called out as "load-bearing" and "never revisit downward" in
  DECISIONS.md, and it is the single biggest gap between what exists now
  and Phase 2's actual exit criterion ("I1–I9 hold under the fault harness
  across 10,000 seeded runs"). What exists instead is 12 example-based
  unit tests covering the same invariants at the unit level — real
  coverage, but categorically not what §6 specifies. This is the most
  important thing to pick up next if continuing Phase 2 rigor rather than
  moving on to Phase 3.

## What would unblock the biggest remaining gaps

1. ~~A running Redis~~ — **done.** `scoop install redis` (native Windows
   port, 8.10.0). Start with `redis-server --port 6379`. Presence and
   typing are now built on it and verified live.
2. **A username/handle column on `users`** — unblocks real `@mention`
   resolution, which several invariants (I6) and features (4.7) depend on.
3. **Deciding OD-2 for real** (build the ProseMirror composer or take the
   Lexical fallback) — currently the single largest piece of Phase 2's
   client-side feature scope with zero progress.
4. **The actual verification suite** (§6/ADR-011) — still the biggest gap
   between what exists and Phase 2's real exit criterion. Unit tests are
   not a substitute for property-based/fault-injection/TLA+ coverage.
