# 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, batched med ~5 sekunders vindu. **Akseptabelt datatap:** Maks 5 sekunder ved hard krasj av SpacetimeDB. Dette er akseptabelt for chat, kanban og show notes. ## 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 2025) ### Ferdig - **SpacetimeDB Rust-modul** (`spacetimedb/src/lib.rs`): `ChatMessage`- og `SyncOutbox`-tabeller. `send_message`-reducer skriver til begge. Publisert som `sidelinja-realtime`. - **Hybrid-adapter i frontend** (`web/src/lib/chat/spacetime.svelte.ts`): Henter historikk fra PG via REST, lytter på SpacetimeDB for sanntidspush. Ingen oppvarming nødvendig — PG har alltid historikken. - **PG-fallback:** Fungerer automatisk. Hvis `VITE_SPACETIMEDB_URL` ikke er satt, brukes ren PG-polling (3 sek intervall). ### Gjenstår - **Sync-worker (§5.1):** Rust-worker som poller `sync_outbox` i SpacetimeDB og batch-skriver til PostgreSQL. Uten denne workeren persisteres meldinger sendt via SpacetimeDB kun i SpacetimeDB-minnet — de overlever ikke restart. - **Oppvarming (§5.2):** Ikke implementert, og hybrid-adapteren gjør dette mindre kritisk (klienten henter alltid PG-historikk uavhengig av SpacetimeDB). - **Workspace-partisjonering (§7):** SpacetimeDB-modulen har `workspace_id`-felt men bruker ikke workspace-token på tilkobling ennå. ### Designvalg tatt - **Hybrid fremfor ren SpacetimeDB:** Frontend bruker PG for historikk og SpacetimeDB kun for nye meldinger. Dette unngår oppvarmingsproblematikk og gir umiddelbar tilgang til all historikk. - **Graceful degradation:** SpacetimeDB-tilkoblingsfeil faller stille tilbake til PG. Brukeren ser ingen feilmelding — PG-data beholdes. - **Adapter-mønster:** `ChatConnection`-interface med to implementasjoner (PG og SpacetimeDB hybrid). Factory velger basert på env-variabel. Gjør det trivielt å teste hver adapter isolert. ## 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