synops/docs/infra/synkronisering.md
vegard 0a467066ba Synops v2: arkitektur, retninger og dokumentasjon
Nystart basert på arkitektonisk innsikt fra Sidelinja v1.
Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid.

Inneholder:
- Komplett arkitekturdokumentasjon (docs/arkitektur.md)
- 6 vedtatte retninger (docs/retninger/)
- Alle concepts, features, proposals og erfaringer fra v1
- Server-oppsett og drift (docs/setup/)
- LiteLLM-konfigurasjon (API-nøkler via env)
- Editor.svelte referanse fra v1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:43:08 +01:00

12 KiB

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 ~1 sekund (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-reducerne legger sync-events i sync_outbox-tabellen med tidsstempel og payload
  • Rust-workeren poller sync_outbox hvert sekund, leser alle usynkede events, skriver til PostgreSQL, og markerer dem som synket via mark_synced-reducer
  • 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 laster Rust-workeren data fra PG per kanal, basert på channels.config:
    • warmup_mode: "all" — alle meldinger i kanalen
    • warmup_mode: "messages" — de N nyeste trådene (komplett med svar)
    • warmup_mode: "days" — alle tråder med aktivitet siste N dager (komplett)
    • warmup_mode: "none" — kanalen hoppes over (arkivert/inaktiv)
  • Trådbasert henting: et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter)
  • Konfigureres per kanal via admin-UI (/admin/channels) eller channels.config JSONB
  • Kanalen ryddes (clear_channel) før lasting for å unngå duplikater ved restart
  • Reaksjoner lastes også inn via load_reactions-reducer

6. Feilhåndtering

  • SpacetimeDB krasjer: Data siden siste synk (~1 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. Workers som endrer data frontend ser

Kritisk regel: En worker (Rust) som transformerer data som frontend viser, MÅ skrive resultatet til SpacetimeDB — ikke direkte til PG. PG-oppdatering skjer via sync-worker i bakgrunnen.

Eksempel: AI-vask av chatmelding

Riktig flyt:

Frontend viser melding fra SpacetimeDB
  → Bruker trykker ✨
  → SvelteKit oppretter jobb i PG job_queue
  → Worker plukker opp jobb
  → Worker leser prompt + routing fra PG (det er infrastrukturdata, ikke frontend-data)
  → Worker kaller AI Gateway
  → Worker skriver resultat til SpacetimeDB via edit_message reducer
  → SpacetimeDB pusher oppdatering til frontend via onUpdate
  → Sync-worker persisterer til PG i bakgrunnen

Feil flyt (anti-pattern som har blitt implementert gjentatte ganger):

Worker skriver til PG direkte
  → Frontend henter metadata fra PG via enrichFromPg()  ← BRYTER ABSTRAKSJONEN
  → SpacetimeDB-oppdatering er "best-effort"            ← FEIL PRIORITERING

Hva betyr dette for nye felter?

Når frontend trenger å vise noe nytt (metadata, revisjoner, edited_at), er prosedyren:

  1. Utvid SpacetimeDB-modulen — legg til feltet i Rust-strukturen
  2. Utvid warmup — last feltet fra PG ved oppstart
  3. Utvid sync — persist feltet til PG i bakgrunnen
  4. Worker skriver til SpacetimeDB — via reducer, aldri direkte PG for synlig data
  5. Frontend leser fra SpacetimeDB — ingen enrichFromPg, ingen PG API-kall

Hva kan workers lese fra PG?

Workers kan og bør lese infrastrukturdata direkte fra PG:

  • Jobbkø (job_queue) — det er jobbens opphavssted
  • AI-prompts, modellkonfigurasjon, routing — det er infrastruktur, ikke brukerdata
  • Workspace-konfigurasjon

Skillet er: data frontend viser → SpacetimeDB. Data kun worker trenger → PG direkte.

10. 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 (med metadata, edited_at), MessageReaction, MessageRevision og SyncOutbox-tabeller. Reducers: send_message, delete_message, edit_message, add_reaction, remove_reaction, load_messages, load_reactions, load_revisions, clear_channel, mark_synced, set_ai_processing, clear_ai_processing, ai_update_message.
  • Worker warmup (worker/src/warmup.rs): Ved oppstart lastes meldinger (med metadata/edited_at), reaksjoner og revisjoner per kanal fra PG → SpacetimeDB. Originale timestamps parses korrekt. 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/ai_update for meldinger og insert/delete for reaksjoner.
  • SpacetimeDB-adapter (web/src/lib/chat/spacetime.svelte.ts): Ren SpacetimeDB-adapter. Null PG API-kall (fetch()). Bruker onInsert/onUpdate/onDelete callbacks for sanntid. Reaksjoner bygges fra message_reaction-tabellen. Revisjoner leses fra message_revision-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.

Fikset (mars 2026)

  • enrichFromPg fjernet — SpacetimeDB-modulen har nå metadata og edited_atChatMessage. Frontend leser alt fra SpacetimeDB, null fetch()-kall i adapteren.
  • Worker AI-vask via reducersset_ai_processing, ai_update_message og clear_ai_processing reducers erstatter direkte PG-skriving. Sync-worker persisterer til PG via ai_update outbox-action.
  • Revisjoner i SpacetimeDBMessageRevision-tabell med load_revisions warmup-reducer. Frontend leser revisjoner via getRevisions() fra conn.db.message_revision.
  • Warmup med metadataload_messages inkluderer metadata, edited_at og parser originale timestamps korrekt.

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.

11. 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