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>
130 lines
10 KiB
Markdown
130 lines
10 KiB
Markdown
# 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
|