# Infrastruktur: PostgreSQL ↔ SpacetimeDB Synkronisering **Filsti:** `docs/infra/synkronisering.md` ## 1. Konsept SpacetimeDB gir sanntidsopplevelsen, PostgreSQL er langtidsminnet. Denne spec-en definerer hvordan data flyter mellom dem, hvem som eier sannheten, og hva som skjer ved feil. ### 1.1 Grunnregel: SpacetimeDB er en ren sanntidsbuffer SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten: 1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller 2. Synkes til PostgreSQL innen ~5 sekunder (chat, kanban, markører). **Konsekvens for design:** Ethvert SpacetimeDB-modul-skjema skal ha en tilsvarende PostgreSQL-tabell som kan fungere som drop-in erstatning dersom SpacetimeDB fjernes. Frontend-kode som leser fra SpacetimeDB skal kunne peke om til et SvelteKit SSE-endepunkt + REST uten arkitekturendring. ### 1.2 PostgreSQL-fallback-prinsippet Dersom SpacetimeDB fjernes fra stacken, skal systemet fungere med følgende erstatning: - **Sanntidsoppdateringer:** PostgreSQL `LISTEN/NOTIFY` → SvelteKit Server-Sent Events (SSE) - **Skriving:** Direkte til PostgreSQL via SvelteKit server-side - **Autocomplete/cache:** PostgreSQL-spørringer med cursor-basert paginering Denne fallbacken trenger ikke implementeres på forhånd, men SpacetimeDB-moduler skal designes slik at fallbacken forblir triviell. ## 2. Strategi: Event-drevet med kort forsinkelse SpacetimeDB-modulene (Rust) produserer persisterings-events ved dataendringer. En Rust-worker konsumerer disse og skriver til PostgreSQL med ~1 sekunds intervall. **Akseptabelt datatap:** Maks 1 sekund ved hard krasj av SpacetimeDB. Dette er akseptabelt for chat, kanban og show notes. **Unntak — kritiske events:** Aha-markører fra studioet (live-innspilling) er tidssensitive og vanskelige å gjenskape. Disse bør flushes til PG umiddelbart (ikke batched) via en dedikert `sync_critical()`-funksjon som skriver direkte til PG i stedet for via `sync_outbox`. Alternativt kan SpacetimeDB-modulen skrive kritiske events til sin egen WAL/disk umiddelbart. Hvilke event-typer som er "kritiske" defineres per workspace i `workspaces.settings`. ## 3. Dataflyt ``` ┌──────────────┐ events ┌──────────────┐ batch write ┌──────────────┐ │ SpacetimeDB │ ──────────────► │ Rust Worker │ ────────────────► │ PostgreSQL │ │ (sanntid) │ │ (sync_to_pg) │ │ (persistent)│ └──────────────┘ └──────────────┘ └──────────────┘ ┌──────────────┐ oppvarming (oppstart / reconnect) ┌──────────────┐ │ PostgreSQL │ ──────────────────────────────────────► │ SpacetimeDB │ └──────────────┘ └──────────────┘ ``` ## 4. Eierskapsmodell | Data | Autoritativ kilde | Synkretning | Merknad | |---|---|---|---| | Chatmeldinger | SpacetimeDB | → PG (event, batched) | | | Kanban-posisjon | SpacetimeDB | → PG (event) | | | Show notes | SpacetimeDB | → PG (event) | | | Live studio-markører | SpacetimeDB | → PG (event) | | | Kunnskapsgraf | PostgreSQL | → SpacetimeDB (oppvarming) | Read-only i SpacetimeDB | | Episodemetadata | PostgreSQL | Ingen synk | | | Brukerkontoer | PostgreSQL (Authentik) | Ingen synk | | | Statistikk | PostgreSQL | Ingen synk | | | Valgomat | TBD | TBD | Konseptet må modnes. Mulig PG-autoritativ med SpacetimeDB som serveringslag | ## 5. Mekanisme ### 5.1 SpacetimeDB → PostgreSQL (persistering) - SpacetimeDB-modulene kaller en intern `emit_sync_event()`-funksjon ved relevante dataendringer - Events bufres i en SpacetimeDB-tabell (`sync_outbox`) med tidsstempel og payload - Rust-workeren poller `sync_outbox` hvert ~5 sekund, leser alle usynkede events, skriver til PostgreSQL i én transaksjon, og markerer dem som synket - Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører ### 5.2 PostgreSQL → SpacetimeDB (oppvarming) - Ved oppstart (eller reconnect) av SpacetimeDB laster Rust-workeren aktive data fra PG: - Aktive temaer med siste N chatmeldinger - Kanban-state for pågående episoder - Aktør/Tema-navn for autocomplete (read-only cache) - Dette er en enveis-last, ikke kontinuerlig synk. Kunnskapsgrafen oppdateres i SpacetimeDB kun ved oppstart eller eksplisitt refresh ## 6. Feilhåndtering - **SpacetimeDB krasjer:** Data siden siste synk (~5 sek) tapes. Ved restart oppvarmes fra PG - **PostgreSQL nede:** Sanntidsfunksjoner fortsetter å fungere. `sync_outbox` vokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk - **Rust-worker krasjer:** `sync_outbox` akkumuleres. Ved restart plukker workeren opp der den slapp (usynkede events har ingen markering) ## 7. Workspace-isolasjon SpacetimeDB-synkronisering er fullstendig workspace-scopet: * SpacetimeDB-tilkoblinger bærer `workspace_id` som context-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace. * `sync_outbox`-events inkluderer `workspace_id` i payloaden, slik at Rust-workeren skriver til riktig silo i PostgreSQL. * Ved oppvarming (PG → SpacetimeDB) laster workeren kun data for den aktuelle workspace-en. ## 8. Konflikthåndtering ### 8.1 Strategi: Last-Write-Wins (LWW) med reservasjoner SpacetimeDB er autoritativ for sanntidsdata. Konflikter kan oppstå i to scenarioer: **Scenario A — Samtidig redigering i SpacetimeDB:** SpacetimeDB er single-threaded per modul. Reducer-funksjoner serialiseres automatisk. Ingen klassiske race conditions. **Scenario B — PG-oppvarming kolliderer med fersk SpacetimeDB-state:** Ved restart av SpacetimeDB lastes data fra PG (oppvarming). Hvis en klient skriver til SpacetimeDB *under* oppvarming, kan PG-dataen overskrive ferske endringer. **Løsning:** Oppvarming setter et `warming_up`-flagg i SpacetimeDB-modulen. Klienttilkoblinger aksepteres, men skrivinger bufres til oppvarmingen er fullført. Buffrede skrivinger appliseres deretter i rekkefølge. ### 8.2 Kanban: Posisjonskonflikter Kanban-kort har en `position`-kolonne (float). To brukere som drar kort samtidig kan skape overlappende posisjoner. Løsning: SpacetimeDB-modulen re-beregner posisjoner (midtpunkts-strategi) og kringkaster oppdatert rekkefølge til alle klienter. ### 8.3 Chat: Ingen konflikter Meldinger er append-only. Redigering av egne meldinger er last-write-wins — akseptabelt fordi kun én bruker eier meldingen. ## 9. Implementeringsstatus (mars 2026) ### Ferdig - **SpacetimeDB som cache foran PG:** PG er autoritativ, SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB. - **SpacetimeDB Rust-modul** (`spacetimedb/src/lib.rs`): `ChatMessage`, `MessageReaction` og `SyncOutbox`-tabeller. Reducers: `send_message`, `delete_message`, `edit_message`, `add_reaction`, `remove_reaction`, `load_messages`, `load_reactions`, `clear_channel`, `mark_synced`. - **Worker warmup** (`worker/src/warmup.rs`): Ved oppstart lastes siste 100 meldinger + reaksjoner per kanal fra PG → SpacetimeDB. Kanaler ryddes først med `clear_channel` for å unngå duplikater. - **Worker sync** (`worker/src/sync.rs`): Poller `sync_outbox` hvert 1. sekund. Håndterer insert/delete/update for meldinger og insert/delete for reaksjoner. - **SpacetimeDB-adapter** (`web/src/lib/chat/spacetime.svelte.ts`): Ren SpacetimeDB-adapter. Ingen PG API-kall. Bruker `onInsert`/`onUpdate`/`onDelete` callbacks for sanntid. Reaksjoner bygges fra `message_reaction`-tabellen. - **PG-fallback** (`web/src/lib/chat/pg.svelte.ts`): Brukes kun når SpacetimeDB ikke er konfigurert. Markert som `readonly: true`. - **Adapter-mønster:** `ChatConnection`-interface med `send`, `edit`, `delete`, `react` metoder. Factory velger basert på env-variabel. ### Gjenstår - **Workspace-partisjonering (§7):** SpacetimeDB-modulen har `workspace_id`-felt men bruker ikke workspace-token på tilkobling ennå. - **Pin/konvertering via SpacetimeDB:** Pin og kanban/kalender-konvertering går fortsatt direkte til PG API. - **Lazy warmup per kanal:** Alle aktive kanaler oppvarmes ved oppstart. Kan optimaliseres til per-kanal ved tilkobling. ## 10. Instruks for Claude Code - `sync_outbox`-tabellen i SpacetimeDB bør ha et `synced`-flagg og `created_at`-tidsstempel - Workeren skal bruke jobbkø-infrastrukturen (se `docs/infra/jobbkø.md`) for sin egen helse/observabilitet, men selve pollingen er en egen loop — ikke en vanlig jobb i køen - Hold sync-payloaden enkel: `{ "table": "chat_messages", "action": "insert", "data": {...} }` — workeren mapper dette til riktig PG-tabell - Ikke optimaliser for store datamengder ennå. Enkle INSERTs er bra nok til volumet stabiliserer seg - Alle sync-payloads må inkludere `workspace_id`. Workeren bruker dette til å sette RLS-kontekst (`SET app.current_workspace_id`) før PG-skriving, eller kjører som superuser og filtrerer eksplisitt