Scribe Docs — offline sync design

Scope. How the client reconciles a scribe doc's content from two local sources (y-indexeddb cache + PowerSync local SQLite) and one remote source (Hocuspocus), across all combinations of online/offline and cache freshness. Companion to the Slice 1 spec — final design after POC validation. Cadence-based diffs (per-checkpoint, not per-onChange). scribe_doc_versions and scribe_doc_contents collapsed into scribe_doc_updates via kind / label / created_by columns.


1. Architecture

flowchart TB
  subgraph Server
    HP["scribe-collab-server<br/>(Node + Hocuspocus)"]
    SB["scribe-backend<br/>(Java)"]
    SDU[("scribe_doc_updates<br/>append-only diffs")]
    MJ["Merge job (TODO)"]
  end
  subgraph Client
    SQ[("PowerSync local SQLite")]
    IDB[("y-indexeddb<br/>Y.Doc state +<br/>lastSeqApplied")]
    YD["Y.Doc"]
    HC["Hocuspocus client"]
    Editor["Editor"]
  end
  HP -->|"per-checkpoint diff"| SB
  SB -->|"INSERT"| SDU
  MJ -.->|"coalesces (post-slice-1)"| SDU
  SDU -->|"PowerSync replication"| SQ
  IDB -->|"phase 1"| YD
  SQ -->|"phase 2"| YD
  HC <-->|"phase 3 + 4"| YD
  YD -->|"persists"| IDB
  YD --> Editor
  HC <==>|"WebSocket"| HP

2. Schema

CREATE TABLE scribe.scribe_doc_updates (
  id                UUID PRIMARY KEY NOT NULL,
  seq               BIGSERIAL        NOT NULL UNIQUE,
  folder_item_id    UUID             NOT NULL REFERENCES scribe.folder_items(id) ON DELETE CASCADE,
  org_id            UUID             NOT NULL REFERENCES public.organizations(id),
  team_id           UUID                      REFERENCES public.teams(id),
  root_folder_id    UUID             NOT NULL REFERENCES scribe.folders(id),
  update_b64        TEXT             NOT NULL,
  state_vector_b64  TEXT             NOT NULL,       -- server-only; excluded from sync rules
  origin            VARCHAR          NOT NULL,       -- 'hocuspocus' | 'merge_job' | 'version_restore'
  kind              VARCHAR          NOT NULL DEFAULT 'auto',  -- 'auto' | 'session_end' | 'manual'
  label             VARCHAR,
  created_by        UUID,
  audit_info        JSONB,
  created_at        TIMESTAMPTZ      NOT NULL DEFAULT now()
);

CREATE INDEX ON scribe.scribe_doc_updates (folder_item_id, seq);
CREATE INDEX ON scribe.scribe_doc_updates (team_id) WHERE team_id IS NOT NULL;
CREATE INDEX ON scribe.scribe_doc_updates (root_folder_id);
CREATE INDEX ON scribe.scribe_doc_updates (folder_item_id, created_at) WHERE kind != 'auto';

3. Server-side diff computation