server/docs/infra/synkronisering.md
vegard 024a91e1b3 Dokumentasjon: arkitekturvurdering — sikkerhet, backup, kostnad, nye forslag
Oppdaterer dokumentasjon basert på tre eksterne arkitekturvurderinger:

- RLS Leak Hunter med CI-test og audit-trigger (migration_safety.md)
- pgvector-migrasjon flyttet til Lag 2, WAL-arkivering med pgBackRest (ARCHITECTURE.md, produksjon.md)
- Off-site backup med rclone, Docker cgroups for workers (ARCHITECTURE.md, produksjon.md)
- Kostnadskontroll i AI Gateway: workspace-budsjett, auto-fallback (ai_gateway.md)
- Gjeste-token sikkerhetsdybde: ClamAV, rate limiting, auto-revoke (den_asynkrone_gjesten.md)
- SpacetimeDB fase 1-vurdering: PG LISTEN/NOTIFY som mellomsteg (synkronisering.md)
- Kritiske events (Aha-markører) flushes umiddelbart (synkronisering.md)
- Ekstern helsesjekk, observability-utvidelser (ARCHITECTURE.md)
- Tre nye forslag: Contradiction Detector, Auto-Highlight Reel, Audience Voice Memo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 03:56:21 +01:00

10 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 ~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.

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

Åpent spørsmål: SpacetimeDB i fase 1?

PG-polling (3 sek) fungerer godt nok for chat og kanban med nåværende brukertall. SpacetimeDB + sync-worker innfører betydelig kompleksitet (outbox, oppvarming, workspace-partisjonering, feilhåndtering) som ennå ikke gir målbar gevinst.

Alternativ: Bruk PostgreSQL LISTEN/NOTIFY → SvelteKit SSE (Server-Sent Events) som neste steg fra polling. Dette gir sub-sekund sanntid uten ny infrastruktur-avhengighet. SpacetimeDB introduseres først når vi har et konkret behov det ikke dekker (f.eks. LiveKit-studio med høyfrekvent state-sync mellom mange klienter).

Beslutning: Utsatt. PG-adapter med polling er "god nok" for Lag 2. SpacetimeDB-koden beholdes men aktiveres ikke i prod før behovet er bevist. Adapter-mønsteret gjør at vi kan bytte uten frontend-endring.

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