- Omorganiser docs/: konsepter, features, infra og proposals i egne mapper - Ny docs/erfaringer/ med lærdommer fra chat-implementering (Svelte 5, SpacetimeDB, adapter-mønster) - Oppdater ARCHITECTURE.md: Lag 1 status, ny §10 Erfaringslogg, SpacetimeDB i lokal dev - Oppdater synkronisering.md med implementeringsstatus og designvalg - Oppdater lokal.md med SpacetimeDB og AI Gateway - Utvid PG-skjema med channels, messages, media_files, message_revisions - Legg til seed_dev.sql, migration_safety.md, .env.example - Nye feature-specs: chat, kanban, whiteboard, live_ai, lydmeldinger m.fl. - Nye konsept-specs: studioet, møterommet, redaksjonen, den asynkrone gjesten m.fl. - SpacetimeDB og AI Gateway i docker-compose.dev.yml - collect-docs.sh inkluderer erfaringer/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.9 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:
- Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller
- 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_outboxhvert ~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_outboxvokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk - Rust-worker krasjer:
sync_outboxakkumuleres. 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_idsom context-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace. sync_outbox-events inkludererworkspace_idi 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- ogSyncOutbox-tabeller.send_message-reducer skriver til begge. Publisert somsidelinja-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_URLikke er satt, brukes ren PG-polling (3 sek intervall).
Gjenstår
- Sync-worker (§5.1): Rust-worker som poller
sync_outboxi 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 etsynced-flagg ogcreated_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