Bygg sanntidssystemer på PostgreSQL: Utover ORM

Atlas Verified-teamet
·
13. februar 2026
·
8 min lesetid
Bygg sanntidssystemer på PostgreSQL: Utover ORM

Når applikasjonen din trenger sanntidsoppdateringer, er standardsvaret i 2025 å velge en administrert tjeneste: Firebase Realtime Database, Supabase Realtime, Pusher, Ably eller et hostet WebSocket-lag. Disse tjenestene skjuler kompleksiteten ved å sende oppdateringer til tilkoblede klienter. De fungerer godt — til de ikke gjør det.

Hos Atlas Verified startet vi med en administrert databaseplattform som inkluderte sanntidsabonnementer. Etter hvert som systemet vokste, traff vi grensene for denne abstraksjonen. Begrensninger i tilkoblingspooling, problemer med sanntidspålitelighet, behovet for romlige PostGIS-forespørsler og ønsket om tilpasset triggerlogikk trakk oss alle i samme retning: ta kontroll over databaselaget.

Vi migrerte til selvhostet PostgreSQL og bygget sanntidssystemet med PostgreSQLs innebygde LISTEN/NOTIFY-mekanisme sammen med Server-Sent Events. Dette innlegget dekker hvorfor vi tok den beslutningen, hvordan arkitekturen fungerer og hva vi lærte.

Hvorfor vi forlot den administrerte plattformen

Administrerte databaseplattformer tilbyr en overbevisende verdipakke: autentisering, sikkerhet på radnivå, sanntidsabonnementer, automatisk genererte API-er og et dashbord — alt klart til bruk. For produkter i en tidlig fase er dette virkelig verdifullt. Du kan bygge en fungerende applikasjon uten å skrive en eneste linje backend-kode.

Problemene oppstår etter hvert som applikasjonen modnes og kravene dine avviker fra plattformens antakelser.

Tilkoblingsgrenser. Administrerte plattformer ruter ofte databaseforbindelser gjennom en tilkoblingspooler som setter egne grenser. Når du har flere backend-tjenester som hver trenger sine egne tilkoblingspooler — en API-server, et bakgrunnsarbeidersystem og en sanntidslytter — bruker du raskt opp de tilgjengelige forbindelsene. Vi endte med å utvikle løsninger rundt poolerens begrensninger i stedet for å løse faktiske produktproblemer.

Sanntidspålitelighet. Den administrerte plattformens sanntidssystem brukte Change Data Capture på PostgreSQLs Write-Ahead Log. Dette er arkitektonisk elegant, men innførte feiltilstander vi ikke kunne kontrollere. WAL-basert replikering har iboende forsinkelse. Sanntidstjenesten kom av og til på etterskudd og leverte utdaterte oppdateringer. Gjenoppkobling etter nettverksavbrudd var upålitelig. Og feilsøking krevde tilgang til infrastruktur vi ikke kontrollerte.

Krav til utvidelser. Applikasjonen vår behandler handelsdokumenter som inneholder geografiske data: havneposisjoner, fraktruter og fartøyposisjoner. PostGIS — den romlige utvidelsen for PostgreSQL — er avgjørende for disse forespørslene. Den administrerte plattformen støttet PostGIS, men vi trengte bestemte versjoner og konfigurasjoner som ikke alltid var tilgjengelige.

Tilpasset triggerlogikk. Vi trengte databasetriggere som utløste applikasjonsspesifikke hendelser — ikke bare generelle «rad endret»-varsler. Når behandlingsstatusen til et dokument endres, må vi varsle den bestemte tilkoblede klienten som lastet det opp. Den administrerte plattformens sanntid fungerte på tabellnivå; vi trengte entitetsnivå.

Migreringsbeslutningen handlet ikke om kostnader eller prinsipper. Den var pragmatisk: vi brukte mer tid på å arbeide rundt plattformens begrensninger enn vi ville ha brukt på å bygge funksjonene selv.

LISTEN/NOTIFY: PostgreSQLs skjulte pub/sub

PostgreSQL har hatt en innebygd publiser/abonner-mekanisme siden versjon 9.0, utgitt i 2010. Til tross for at LISTEN/NOTIFY er over 15 år gammel, brukes den bemerkelsesverdig lite. De fleste utviklere vet ikke at den finnes.

Mekanismen er enkel. Enhver databaseforbindelse kan utføre LISTEN på en navngitt kanal for å abonnere. Enhver forbindelse kan utføre NOTIFY for å sende en melding til alle lyttere på den kanalen. Nyttelasten er en streng, begrenset til 8,000 byte.

Hva som gjør LISTEN/NOTIFY kraftfull for sanntidsapplikasjoner:

Ingen ekstra infrastruktur. Det finnes ingen separat meldingsmegler som må distribueres, overvåkes eller skaleres. Har du PostgreSQL, har du pub/sub. Ingen Redis, ingen RabbitMQ, ingen Kafka — for akkurat dette bruksområdet.

Transaksjonsgarantier. NOTIFY-meldinger leveres bare når den omsluttende transaksjonen bekreftes. Hvis en transaksjon setter inn en rad og sender et varsel, og transaksjonen rulles tilbake, leveres aldri varselet. Dette er en egenskap du ikke får fra eksterne meldingsmeglere uten nøye koordinering.

Triggerintegrasjon. PostgreSQL-triggere kan kalle pg_notify som en del av utførelsen. Dette betyr at du kan la databasen automatisk varsle lyttere hver gang bestemte data endres, uten applikasjonskode i skrivebanen.

Begrensningene er reelle og bør forstås:

Ingen varig lagring. Hvis ingen lytter når en NOTIFY utløses, går meldingen tapt. Det finnes ingen avspilling på nytt, ingen meldingskø og ingen leveringsgarantier for frakoblede klienter. Lytterne dine må håndtere gjenoppkobling og gjenoppretting av tilstand.

Nyttelaststørrelse. Grensen på 8,000 byte for varslingsnyttelast betyr at du ikke kan sende store objekter gjennom NOTIFY. Vi sender minimale nyttelaster — tabellnavn, operasjonstype og primærnøkkel — og lar klienten hente de fullstendige dataene ved behov.

Avgrenset til én database. LISTEN/NOTIFY fungerer innenfor én PostgreSQL-instans. Den spenner ikke over replikaer eller klynger. For arkitekturer der alle skrivinger går til én primærinstans, er dette ingen begrensning.

Arkitekturen vår: Triggere, SSE og kanalmultipleksing

Slik passer delene sammen.

Databasetriggere installeres på tabeller der sanntidsoppdateringer betyr noe: dokumenter, meldinger, samtaler, varsler, forsendelsesverifiseringer og mer. Når en rad settes inn, oppdateres eller slettes, utløses triggeren og kaller pg_notify med en JSON-nyttelast som inneholder tabellnavn, operasjonstype, primærnøkkel og eventuelle filterkolonner.

En dedikert lytterforbindelse i API-serveren vår utfører LISTEN på varslingskanalen og mottar alle triggerhendelser. Dette er en langvarig, dedikert PostgreSQL-forbindelse — separat fra tilkoblingspoolen for forespørsler — som ikke gjør annet enn å lytte etter varsler.

En hendelsesbuss mottar varsler fra lytteren og ruter dem til tilkoblede klienter basert på abonnementene deres. En klient som abonnerer på dokumenter der en bestemt forsendelses-ID samsvarer, mottar bare varsler om dokumenter i akkurat den forsendelsen.

Server-Sent Events leverer varsler til nettleseren. Hver tilkoblede klient åpner én SSE-forbindelse til API-serveren og angir hvilke kanaler og filtre den ønsker. Hendelsesbussen sammenholder innkommende varsler med klientabonnementer og sender samsvarende hendelser gjennom de riktige SSE-forbindelsene.

Hvorfor SSE fremfor WebSockets

Dette er en beslutning vi konsekvent har vært fornøyde med. Server-Sent Events er enklere enn WebSockets i hver dimensjon som betyr noe for bruksområdet vårt.

Enveis av design. Sanntidsbehovene våre går fra server til klient: serveren forteller klienten når data endres. Klienten trenger ikke å sende sanntidsmeldinger til serveren — den bruker vanlige HTTP-forespørsler til det. SSEs enveismodell samsvarer med det faktiske behovet vårt.

HTTP-basert. SSE-forbindelser er vanlige HTTP-forespørsler med en bestemt innholdstype. De fungerer gjennom enhver HTTP-proxy, lastbalanserer og CDN uten spesiell konfigurasjon. WebSockets krever HTTP Upgrade, som noen proxyer ikke håndterer riktig og enkelte bedriftsbrannmurer blokkerer fullstendig.

Automatisk gjenoppkobling. Nettleserens EventSource API håndterer gjenoppkobling innebygd. Hvis forbindelsen brytes, kobler nettleseren automatisk til igjen med eksponentielt økende ventetid. Med WebSockets implementerer du gjenoppkoblingslogikken selv — og gjør feil minst én gang.

Det ene området der WebSockets vinner — toveis kommunikasjon — trenger vi ganske enkelt ikke. For applikasjoner som krever sanntidsmeldinger fra klient til server, er WebSockets riktig valg. For applikasjoner som sender varsler fra server til klient, er SSE riktig valg.

Bygg et spørrelag uten ORM

Da vi forlot den administrerte plattformen, forlot vi også dens automatisk genererte spørre-API. Vi trengte et databasetilgangslag for API-serveren vår.

Standardalternativene er ORM-er som Prisma, TypeORM eller Drizzle — eller rå SQL med en driver. Vi valgte en tredje vei: en lett spørrebygger som tilbyr et kjedbart API, samtidig som den genererer og utfører rå SQL under overflaten.

Kjent API. Den eksisterende kodebasen vår var skrevet mot den administrerte plattformens klientbibliotek. Et kompatibelt API betydde minimale kodeendringer under migreringen. Forespørsler som tidligere kjørte gjennom plattformens klient, kunne kjøre gjennom spørrebyggeren vår med identisk syntaks.

Ingen magi. ORM-er genererer SQL du ikke kan se og noen ganger ikke kan kontrollere. Når en forespørsel er langsom, må du lese den genererte SQL-en, som ofte ligner lite på ORM-kallet som produserte den. Spørrebyggeren vår genererer forutsigbar SQL — hver kjedede metode tilsvarer en bestemt SQL-klausul, og du kan alltid inspisere den genererte forespørselen.

Innebygde joins. Handelsdata er relasjonelle. En forsendelse har dokumenter, som har noder, som har verifiseringskontroller. Å kunne uttrykke nestede utvalg og få tilbake et nestet resultat — uten å skrive en kompleks JOIN-forespørsel for hånd hver gang — er en betydelig forbedring av utvikleropplevelsen.

Resultatet er et spørrelag på noen hundre kodelinjer som håndterer det store flertallet av databaseinteraksjonene våre og produserer SQL vi alltid kan lese og optimalisere.

Budsjettering av tilkoblingspooler

Når du selv hoster PostgreSQL, har du ansvar for tilkoblingsbudsjettet. PostgreSQLs max_connections-innstilling definerer den absolutte grensen, og hver forbindelse bruker minne.

Vi oppdaget at å tenke på forbindelser som et budsjett — en begrenset ressurs delt mellom tjenester — forhindrer en klasse problemer som rammer team som ikke planlegger for det.

Hver tjeneste oppgir sitt maksimale tilkoblingsantall. Summen av alle maksimumsverdier må holde seg godt under PostgreSQLs grense, med rom for forbindelser etter behov — psql-økter, migreringsskript og overvåkingsverktøy. Dette høres åpenbart ut, men de fleste team gjør det ikke før de går tom for forbindelser i produksjon. Ved å budsjettere forbindelser på forhånd har vi unngått akkurat den alarmen klokken 3 om morgenen.

Sikre migreringer i stor skala

Migrering fra en administrert plattform til selvhostet PostgreSQL betydde å flytte hundrevis av tabeller, funksjoner, triggere og indekser. Men den større utfordringen var å bygge et migreringssystem som ville fortsette å fungere etter hvert som applikasjonen utviklet seg.

Det automatiske migreringssystemet vårt fungerer slik: migreringsfiler navngis med tidsstempler og ligger i en egen mappe. En migreringskjører skanner mappen, sammenligner med en sporingstabell som registrerer hvilke migreringer som er utført, og utfører ventende migreringer i rekkefølge. Hver migrering kjører inne i en transaksjon — hvis den mislykkes, rulles den ryddig tilbake og blokkerer utrullingen til problemet er løst.

Migreringskjøreren utføres automatisk ved hver utrulling, før applikasjonen starter. Dette betyr at skjemaendringer brukes som en del av utrullingsprosessen, uten manuelle trinn. En utvikler oppretter en tidsstemplet SQL-fil, sender den til repositoriet, og neste utrulling bruker den.

Viktige designbeslutninger som holder dette trygt: migreringer bruker idempotente beskyttelser slik at de ikke mislykkes hvis de er delvis utført. Hver migrering må være et selvstendig SQL-skript som kjører uten menneskelig inndata. Og hvis en migrering mislykkes, rulles transaksjonen helt tilbake — applikasjonen starter ikke med en delvis utført skjemaendring.

Hva vi oppnådde

Å gå over til selvhostet PostgreSQL med tilpasset sanntid var en betydelig utviklingsinvestering. Men gevinsten har vært stor:

Pålitelighet. Sanntidsvarsler kommer innen millisekunder etter den underliggende dataendringen. Ingen WAL-replikeringsforsinkelse, ingen nedetid i tredjepartstjenester på den kritiske banen.

Mulighet for feilsøking. Når noe går galt, kan vi spore hele banen: triggeren utløses, varselet sendes, hendelsesbussen ruter og SSE leverer. Hvert trinn finnes i koden vår, i loggene våre og på infrastrukturen vår.

Fleksibilitet. Å legge til sanntid for en ny tabell er én SQL-migrering som installerer en trigger. Ingen plattformkonfigurasjon, intet tredjepartsdashbord og ingen støttehenvendelse.

Ytelse. Direkte PostgreSQL-forbindelser med en tilpasset pool gjør det bedre enn rutede forbindelser gjennom en administrert plattforms infrastruktur. Forespørselsforsinkelsen falt merkbart etter migreringen.

Avveiningen er driftsansvar. Vi administrerer databasen, håndterer sikkerhetskopier, overvåker replikering og planlegger kapasitet. For et team som er komfortabelt med PostgreSQL-drift, er dette håndterbart. For et team som ikke er det, kan den administrerte plattformens abstraksjoner være verdt begrensningene.


Atlas Verified kjører på selvhostet PostgreSQL med tilpasset sanntidsinfrastruktur og behandler tusenvis av verifiseringer av handelsetterlevelse daglig gjennom den globale forsyningskjeden.

Likte du denne artikkelen?

Del den med nettverket ditt