Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/specifications/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ One long-running local Node process, no separate backend/frontend deployment:
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ │
│ │ In-memory Y.Doc (per shard) │ │
│ │ In-memory Y.Doc (Phase 0: one │ │
│ │ global shard) │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ▼ │
Expand All @@ -42,6 +43,19 @@ One long-running local Node process, no separate backend/frontend deployment:

**The Y.Doc/Awareness pair is resolved, not global.** Every boundary that touches workspace state — the `/ws` WebSocket handler, each `services/*.ts` function, and the read-only SvelteKit route loads — calls `resolveWorkspaceContext()` (`src/lib/server/workspace-store.ts`) rather than reaching a bare process-global singleton. That function is a real `{workspaceId, shardId} → {doc, awareness, connections}` registry: it's already exercised with more than one key in `workspace-store.test.ts`, proving two resolved contexts never share CRDT state, Awareness state, or persisted snapshot rows. Phase 0 itself only ever has one real workspace and no auth to pick a different one, so every current boundary calls `resolveWorkspaceContext()` with no selector and always gets the same default context back — a client-supplied value (e.g. the WebSocket room path segment) is accepted as a forward-compatible selector but is never treated as authority. This is the seam #13 (Y.Doc sharding strategy) and #6 (multi-space) build on: adding real per-workspace routing is a change to what selector a boundary resolves with, not a rewrite of how state is reached. See issue #30.

**Capacity boundary (confirmed 2026-08-30).** The Phase 0 global shard is a
deliberate, bounded implementation, not the final multi-space architecture.
The [CRDT capacity baseline](../benchmarks/crdt-capacity-baseline-2026-08-30.md)
confirms it is suitable for a small daily workspace, while its larger profile
shows that every connected client pays for the whole global state. The next
architecture boundary is therefore document- and Collection-level Yjs shards,
with workspace navigation/catalog updates kept outside those content shards.
That is a direction validated by the baseline, not a claim that shard-aware
routing is shipped: Phase 0 still resolves every live connection to the one
default shard. #112 defines the proposed catalog/shard design, and #113 must
prove it with real per-shard transport measurements before this architecture
can describe it as implemented.

**Why HTTP MCP transport, not stdio:** Claude Desktop, Claude Code, and ChatGPT all support pointing at a remote MCP server via URL + bearer token in their own config (per the docs linked in the PRD). That matches the "simple local access token" decision better than stdio, which would mean each client spawns and manages its own subprocess — more moving parts for no benefit when everything's already running as one long-lived local service.

## 2. App structure (SvelteKit)
Expand Down
2 changes: 1 addition & 1 deletion docs/specifications/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

[Drizzle](https://orm.drizzle.team/) (Apache 2.0 — same license as this project's own goal, not just compatible with it) manages every SQLite table, including the read model in §2 — one schema/migration story (`drizzle-kit`) instead of hand-written SQL for some tables and a separate library for others. It's also the SvelteKit-idiomatic choice: the Svelte CLI ships a built-in `drizzle` setup command.

- `snapshots` — periodic binary snapshots of the `Y.Doc` state (via `Y.encodeStateAsUpdate`). Simpler than an update-log replay for now; revisit if snapshot size becomes a problem. Keyed by `(workspace_id, shard_id)` (`src/lib/server/workspace-store.ts`'s `DEFAULT_WORKSPACE_ID`/`DEFAULT_SHARD_ID` today — see `architecture.md` §1) — every read/write/retention-prune query in `store.ts`'s `getSnapshotStore()` scopes by that key, so a second workspace's snapshots, once #13/#6 land, can never collide with or prune the first's. A given key's snapshot is loaded lazily, the first time `resolveWorkspaceContext()` resolves that workspace/shard — not eagerly for every context on process start, since Phase 0 only ever resolves the one default key anyway.
- `snapshots` — periodic binary snapshots of the `Y.Doc` state (via `Y.encodeStateAsUpdate`). Simpler than an update-log replay for now; revisit if snapshot size becomes a problem. Keyed by `(workspace_id, shard_id)` (`src/lib/server/workspace-store.ts`'s `DEFAULT_WORKSPACE_ID`/`DEFAULT_SHARD_ID` today — see `architecture.md` §1) — every read/write/retention-prune query in `store.ts`'s `getSnapshotStore()` scopes by that key, so a second workspace's snapshots, once #13/#6 land, can never collide with or prune the first's. A given key's snapshot is loaded lazily, the first time `resolveWorkspaceContext()` resolves that workspace/shard — not eagerly for every context on process start, since Phase 0 only ever resolves the one default key anyway. The [2026-08-30 capacity baseline](../benchmarks/crdt-capacity-baseline-2026-08-30.md) confirms this snapshot path for a small daily workspace, but also establishes a global-snapshot budget: at 2 MiB or more, or when the associated event-loop p99 reaches 100 ms, prioritize document/Collection sharding or compaction rather than extending the Phase 0 global-state envelope. Those are engineering escalation signals, not a claim that either future design is already implemented.
- `audit_log` — append-only: `id, actor_json, action, target_record_id, timestamp, diff_json`. Populated on every write/delete tool call, denied MCP attempt, and UI-originated write — including UI writes that bypass the service layer entirely, via a generic `Y.Doc`-level observer — satisfying the PRD's audit-log requirement; see [`audit-coverage.md`](./audit-coverage.md) for exactly what's covered, what's deliberately excluded, and how attribution and volume are handled.
- `access_tokens` — `token_hash, client_label, allowed_document_ids, allowed_collection_ids, created_at`. `client_label` is a free-text field set when the token is created (e.g., "Claude Desktop") — this is what powers the "Brylie · via Claude Desktop" attribution tag, since there's no per-vendor OAuth to source it from otherwise.

Expand Down
Loading