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>
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:
- Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller
- 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_outboxhvert sekund, leser alle usynkede events, skriver til PostgreSQL, og markerer dem som synket viamark_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 kanalenwarmup_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) ellerchannels.configJSONB - 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_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. 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:
- Utvid SpacetimeDB-modulen — legg til feltet i Rust-strukturen
- Utvid warmup — last feltet fra PG ved oppstart
- Utvid sync — persist feltet til PG i bakgrunnen
- Worker skriver til SpacetimeDB — via reducer, aldri direkte PG for synlig data
- 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(medmetadata,edited_at),MessageReaction,MessageRevisionogSyncOutbox-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 medclear_channelfor å unngå duplikater. - Worker sync (
worker/src/sync.rs): Pollersync_outboxhvert 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()). BrukeronInsert/onUpdate/onDeletecallbacks for sanntid. Reaksjoner bygges framessage_reaction-tabellen. Revisjoner leses framessage_revision-tabellen. - PG-fallback (
web/src/lib/chat/pg.svelte.ts): Brukes kun når SpacetimeDB ikke er konfigurert. Markert somreadonly: true. - Adapter-mønster:
ChatConnection-interface medsend,edit,delete,reactmetoder. Factory velger basert på env-variabel.
Fikset (mars 2026)
enrichFromPgfjernet — SpacetimeDB-modulen har nåmetadataogedited_atpåChatMessage. Frontend leser alt fra SpacetimeDB, nullfetch()-kall i adapteren.- Worker AI-vask via reducers —
set_ai_processing,ai_update_messageogclear_ai_processingreducers erstatter direkte PG-skriving. Sync-worker persisterer til PG viaai_updateoutbox-action. - Revisjoner i SpacetimeDB —
MessageRevision-tabell medload_revisionswarmup-reducer. Frontend leser revisjoner viagetRevisions()fraconn.db.message_revision. - Warmup med metadata —
load_messagesinkluderermetadata,edited_atog 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 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