MeridiansMeridians

6. Goal & constraints

Source path: knowledge-base/knowledge/architecture/persistence/sync-foundations.md

# 6. Goal & constraints

- **One world, many people.** A Director runs Meridians locally; teammates/admins reach the *same
  instance* over an ngrok tunnel and work together in real time.
- **Sovereign / local-first.** No cloud backend. The Director's machine is the system of record (Part A):
  the record on disk is canon; the only server is the Director's own Next process, which the tunnel
  proxies.
- **Single source of truth.** The **master** owns the canonical record. Every change is reconciled
  through it, so there is one canonical history.
- **Fast reads, deliberate writes.** Viewing is local and instant on every node; only writes pay a
  round-trip.
- **Admins ≈ Directors (for now).** Clients are treated as admin-privileged members with full
  capability. Permissions can be tightened later at a small number of gates.

# 7. Roles

| Role | Who | Authority |
|---|---|---|
| **Master** | The Director's browser on `localhost`/`127.0.0.1` | Owns the canonical **record** (fs, Part A); authoritative reducer; hosts the broker |
| **Client** *(prose: "clone")* | Any other origin (the tunnel) | Local IndexedDB replica + reducer; optimistic edits pushed to the master |

Role is decided **by origin**, once per session — `getSyncRole()` (`src/lib/server/sync/role.ts`).
Localhost ⇒ master, everything else ⇒ client. The browser only reaches its own daemon when
`daemonEligible()` (master + not latched-off + in a window); the broker enforces a single host as a
backstop (a second localhost tab is displaced, see §16).

# 8. Topology

```
        ngrok tunnel (one public URL → the Director's Next process)
                              │
   client browsers ──────────┼──────────── master browser (localhost)
   (IndexedDB replica)        │             (owns the canonical record — Part A)
            │   SSE down / HTTP up          │  SSE host stream / HTTP publish
            └──────────►  in-process BROKER ◄──────────┘
                     (globalThis, src/lib/server/sync/broker.ts)
```

One Next process. The **broker** is an in-process hub on `globalThis` (survives HMR). It holds NO
authority — it relays messages, retains the lightweight entries/open-games registries, and keeps a
byte-bounded asset working set. Full Domain and Constellation documents are live frames, not server-cache
residents; a reconnect requests the fresh canonical document from the master. The tunnel proxies the same
process, so clients reach the exact broker the master hosts.

# 9. The broker — `src/lib/server/sync/broker.ts`

- `host: Sink | null` — the single master SSE sink (single-host; last localhost wins, prior host gets a
  `displaced` frame).
- `clients: Set<Sink>` + `cloneIds` + `presence` — client connections, their ids, and their live "what am
  I viewing" rows (the Director roster).
- `entries` + `openGames` — lightweight reconnect state. Full documents are requested from the master
  and fanned out without being retained by the broker.
- `assets` + `assetWaiters` + `batchWaiters` — content-asset cache and the pending request/response
  correlation for the asset relay.
- Functions: `subscribeHost`, `subscribeClient`, `relayToHost`, `publishDomain`, `publishEntries`,
  `publishRemoved`, `publishReject`, `requestAsset`, `requestAssets`, `provideAsset`/`provideAssets`,
  `updatePresence`, `disconnectAllClients`.

# 10. Wire protocol — `src/lib/server/sync/protocol.ts`

Two HTTP routes carry everything: `POST /api/sync` (messages), `GET /api/sync/stream` (SSE; `?role=host`
for the master, else a client, with `cloneId`). Asset bytes use `GET /api/sync/asset?id=` (single) and
`POST /api/sync/assets` (bulk).

- **Client → master (relayed to host):** `action` (a domain edit + target domain/branch + `pushId`),
  `create` (new full domain), `delete`, `request` (lazy pull a domain), `pull` (ask for the entry list),
  `asset-upload` (a client-created asset), `presence`.
- **Master → broker → clients:** `domain` (canonical doc, optional `ackPushId`), `entries`, `removed`,
  `reject`, `disconnect`, `asset-data`/`asset-data-batch`. The broker itself fans `asset-notice`
  (record → clients; id-only, member-gated — see §12).
- **Broker → host:** `roster`, `asset-request`/`asset-request-batch`, plus the relayed client kinds.
  `displaced` to a superseded host.
Open on GitHub

Raw Markdown source