När en applikation behöver realtidsuppdateringar är standardsvaret 2025 en förvaltad tjänst: Firebase Realtime Database, Supabase Realtime, Pusher, Ably eller ett hostat WebSocket-lager. De döljer komplexiteten i att skicka uppdateringar till anslutna klienter. De fungerar bra — tills de inte gör det.
Atlas Verified började på en förvaltad databasplattform med realtidsprenumerationer. När systemet växte nådde vi abstraktionens gränser. Anslutningspoolbegränsningar, realtidsproblem, PostGIS-behov och egen utlösarlogik förde oss mot samma mål: kontroll över databaslagret.
Vi flyttade till egenhostad PostgreSQL och byggde realtid med den inbyggda LISTEN/NOTIFY-mekanismen och Server-Sent Events. Artikeln beskriver beslutet, arkitekturen och lärdomarna.
Varför vi lämnade den förvaltade plattformen
Förvaltade databasplattformar ger ett attraktivt erbjudande: autentisering, säkerhet på radnivå, realtidsprenumerationer, automatiska API:er och instrumentpanel — färdigt från start. Det är värdefullt för tidiga produkter. En fungerande applikation kan byggas utan en rad serverkod.
Problemen kommer när applikationen mognar och kraven avviker från plattformens antaganden.
Anslutningsgränser. Förvaltade plattformar leder ofta databasanslutningar genom en poolhanterare med egna gränser. När flera serverdelstjänster behöver egna pooler — API-server, bakgrundsarbetare och realtidslyssnare — tar anslutningarna snabbt slut. Vi utvecklade kring poolens begränsningar i stället för att lösa verkliga produktproblem.
Realtidstillförlitlighet. Den hanterade plattformens realtidssystem fungerade genom Change Data Capture på PostgreSQLs Write-Ahead Log. Arkitekturen är elegant, men den medförde feltyper som vi inte kunde kontrollera. WAL-baserad replikering har en inneboende fördröjning. Realtidstjänsten hamnade ibland efter och levererade inaktuella uppdateringar. Återanslutningen efter nätverksavbrott var opålitlig. Och felsökning av dessa problem krävde tillgång till infrastruktur som vi inte kontrollerade.
Krav på tillägg. Vår applikation behandlar handelsdokument som innehåller geografiska data: hamnlägen, sjöfartsrutter och fartygspositioner. PostGIS — det spatiala tillägget för PostgreSQL — är avgörande för dessa frågor. Den hanterade plattformen stödde PostGIS, men vi behövde specifika versioner och konfigurationer som inte alltid var tillgängliga.
Anpassad triggerlogik. Vi behövde databastriggrar som utlöste applikationsspecifika händelser — inte bara allmänna meddelanden om att en ”rad har ändrats”. När ett dokuments behandlingsstatus ändras behöver vi meddela just den anslutna klient som laddade upp det. Den hanterade plattformens realtidsfunktion låg på tabellnivå; vi behövde den på entitetsnivå.
Beslutet att migrera handlade inte om kostnad eller principer. Det var pragmatiskt: vi lade mer tid på att kringgå plattformens begränsningar än vi skulle ha lagt på att bygga funktionerna själva.
LISTEN/NOTIFY: PostgreSQLs dolda publicerings- och prenumerationsfunktion
PostgreSQL har haft en inbyggd mekanism för publicering och prenumeration sedan version 9.0, som släpptes 2010. Trots att LISTEN/NOTIFY är över 15 år gammalt används det förvånansvärt lite. De flesta utvecklare vet inte att det finns.
Mekanismen är enkel. Vilken databasanslutning som helst kan köra LISTEN på en namngiven kanal för att prenumerera. Vilken anslutning som helst kan köra NOTIFY för att skicka ett meddelande till alla lyssnare på den kanalen. Nyttolasten är en sträng, begränsad till 8,000 byte.
Det här gör LISTEN/NOTIFY kraftfullt för realtidsapplikationer:
Ingen extra infrastruktur att hantera. Det finns ingen separat meddelandeförmedlare att driftsätta, övervaka eller skala. Har du PostgreSQL har du publicering och prenumeration. Ingen Redis, ingen RabbitMQ, ingen Kafka — för just detta användningsfall.
Transaktionsgarantier. NOTIFY-meddelanden levereras först när den omgivande transaktionen bekräftas. Om en transaktion infogar en rad och skickar en avisering, men sedan återställs, levereras aviseringen aldrig. Det är en egenskap som externa meddelandeförmedlare inte erbjuder utan noggrann samordning.
Integration med triggrar. PostgreSQL-triggrar kan anropa pg_notify som en del av sin körning. Det innebär att databasen automatiskt kan meddela lyssnare när specifika data ändras, utan någon applikationskod i skrivflödet.
Begränsningarna är verkliga och värda att förstå:
Ingen beständig lagring. Om ingen lyssnar när ett NOTIFY utlöses går meddelandet förlorat. Det finns ingen återuppspelning, ingen meddelandekö och inga leveransgarantier för frånkopplade klienter. Dina lyssnare måste hantera återanslutning och återställning av tillstånd.
Nyttolastens storlek. Gränsen på 8,000 byte för aviseringsnyttolaster innebär att du inte kan skicka stora objekt genom NOTIFY. Vi skickar minimala nyttolaster — tabellnamn, operationstyp och primärnyckel — och låter klienten hämta fullständiga data vid behov.
Omfattar en enda databas. LISTEN/NOTIFY fungerar inom en enda PostgreSQL-instans. Det sträcker sig inte över repliker eller kluster. För arkitekturer där alla skrivningar går till en enda primär instans är detta ingen begränsning.
Vår arkitektur: triggrar, SSE och kanalmultiplexering
Så här hänger delarna ihop.
Databastriggrar installeras på tabeller där realtidsuppdateringar är viktiga: dokument, meddelanden, konversationer, aviseringar, leveransverifieringar med mera. När en rad infogas, uppdateras eller raderas utlöses triggern och anropar pg_notify med en JSON-nyttolast som innehåller tabellnamn, operationstyp, primärnyckel och eventuella filterkolumner.
En särskild lyssnaranslutning i vår API-server kör LISTEN på aviseringskanalen och tar emot alla triggerhändelser. Detta är en långlivad, särskild PostgreSQL-anslutning — separat från anslutningspoolen för frågor — som enbart lyssnar efter aviseringar.
En händelsebuss tar emot aviseringar från lyssnaren och skickar dem vidare till anslutna klienter utifrån deras prenumerationer. En klient som prenumererar på dokument där ett specifikt leverans-ID matchar får bara aviseringar om dokument i just den leveransen.
Server-Sent Events levererar aviseringar till webbläsaren. Varje ansluten klient öppnar en enda SSE-anslutning till API-servern och anger vilka kanaler och filter den vill ha. Händelsebussen matchar inkommande aviseringar mot klienternas prenumerationer och skickar matchande händelser genom de relevanta SSE-anslutningarna.
Varför SSE framför WebSockets
Det är ett beslut som vi genomgående har varit nöjda med. Server-Sent Events är enklare än WebSockets i varje avseende som är relevant för vårt användningsfall.
Enkelriktat från grunden. Våra realtidsbehov går från server till klient: servern meddelar klienten när data ändras. Klienten behöver inte skicka realtidsmeddelanden till servern — den använder vanliga HTTP-anrop för det. SSEs enkelriktade modell motsvarar vårt faktiska behov.
Bygger direkt på HTTP. SSE-anslutningar är vanliga HTTP-anrop med en specifik innehållstyp. De fungerar genom alla HTTP-proxyservrar, lastbalanserare och CDN:er utan särskild konfiguration. WebSockets kräver HTTP Upgrade, som vissa proxyservrar inte hanterar korrekt och som vissa företagsbrandväggar blockerar helt.
Automatisk återanslutning. Webbläsarens EventSource-API hanterar återanslutning direkt. Om anslutningen bryts återansluter webbläsaren automatiskt med exponentiellt ökande väntetid. Med WebSockets implementerar du återanslutningslogiken själv — och gör fel minst en gång.
Det enda område där WebSockets vinner — dubbelriktad kommunikation — behöver vi helt enkelt inte. För applikationer som kräver realtidsmeddelanden från klient till server är WebSockets rätt val. För applikationer som skickar aviseringar från server till klient är SSE rätt val.
Bygga ett frågelager utan ORM
När vi lämnade den hanterade plattformen lämnade vi också dess automatiskt genererade fråge-API. Vi behövde ett databasåtkomstlager för vår API-server.
Standardalternativen är ORM:er som Prisma, TypeORM eller Drizzle — eller direkt SQL med en drivrutin. Vi valde en tredje väg: en lättviktig frågebyggare som erbjuder ett API med kedjebara anrop och samtidigt genererar och kör direkt SQL under ytan.
Ett välbekant API. Vår befintliga kodbas var skriven för den hanterade plattformens klientbibliotek. Ett kompatibelt API innebar minimala kodändringar under migreringen. Frågor som tidigare kördes genom plattformens klient kunde köras genom vår frågebyggare med identisk syntax.
Ingen magi. ORM:er genererar SQL som du inte kan se och ibland inte kan kontrollera. När en fråga är långsam behöver du läsa den genererade SQL-koden, som ofta har liten likhet med ORM-anropet som skapade den. Vår frågebyggare genererar förutsägbar SQL — varje metod i kedjan motsvarar en specifik SQL-sats, och du kan alltid granska den genererade frågan.
Inbäddade join-operationer. Handelsdata är relationella. En leverans har dokument, som har noder, som har verifieringskontroller. Att kunna uttrycka nästlade urval och få tillbaka ett nästlat resultat — utan att skriva en komplex JOIN-fråga för hand varje gång — förbättrar utvecklarupplevelsen avsevärt.
Resultatet är ett frågelager på några hundra kodrader som hanterar den överväldigande majoriteten av våra databasinteraktioner och producerar SQL som vi alltid kan läsa och optimera.
Budgetering av anslutningspoolen
När du själv driftar PostgreSQL ansvarar du för anslutningsbudgeten. PostgreSQLs inställning max_connections definierar det absoluta taket, och varje anslutning förbrukar minne.
Vi upptäckte att synen på anslutningar som en budget — en begränsad resurs som delas mellan tjänster — förebygger en typ av problem som drabbar team som inte planerar för detta.
Varje tjänst anger sitt maximala antal anslutningar. Summan av alla maximum måste ligga klart under PostgreSQLs gräns, med utrymme för tillfälliga anslutningar — psql-sessioner, migreringsskript och övervakningsverktyg. Det låter självklart, men de flesta team gör det inte förrän anslutningarna tar slut i produktion. Genom att budgetera anslutningarna i förväg har vi undvikit just det där larmet klockan 3 på natten.
Säkra migreringar i stor skala
Att migrera från en hanterad plattform till egen drift av PostgreSQL innebar att flytta hundratals tabeller, funktioner, triggrar och index. Men den större utmaningen var att bygga ett migreringssystem som skulle fortsätta fungera när applikationen utvecklades.
Vårt automatiserade migreringssystem fungerar så här: migreringsfiler får tidsstämplar i namnen och ligger i en särskild katalog. Ett migreringsverktyg skannar katalogen, jämför med en spårningstabell som registrerar vilka migreringar som har tillämpats och kör alla återstående migreringar i ordning. Varje migrering körs inom en transaktion — misslyckas den återställs allt ordentligt och driftsättningen blockeras tills problemet har åtgärdats.
Migreringsverktyget körs automatiskt vid varje driftsättning, innan applikationen startar. Det innebär att schemaändringar tillämpas som en del av driftsättningen, utan manuella steg. En utvecklare skapar en SQL-fil med tidsstämpel, skickar den till kodarkivet och nästa driftsättning tillämpar den.
Viktiga designbeslut som gör detta säkert: migreringar använder idempotenta skydd så att de inte misslyckas om de har tillämpats delvis. Varje migrering måste vara ett fristående SQL-skript som körs utan mänsklig inmatning. Och om en migrering misslyckas återställs hela transaktionen — applikationen startar inte med en delvis tillämpad schemaändring.
Vad vi vann
Övergången till egen drift av PostgreSQL med anpassad realtidsfunktion var en betydande teknisk investering. Men resultaten har varit omfattande:
Tillförlitlighet. Realtidsaviseringar kommer inom millisekunder efter att underliggande data ändrats. Ingen fördröjning i WAL-replikeringen och inga avbrott hos tredjepartstjänster i det kritiska flödet.
Möjlighet att felsöka. När något går fel kan vi spåra hela förloppet: triggern utlöses, aviseringen skickas, händelsebussen dirigerar den och SSE levererar. Varje steg finns i vår kod, i våra loggar och i vår infrastruktur.
Flexibilitet. Att lägga till realtidsfunktion för en ny tabell kräver en enda SQL-migrering som installerar en trigger. Ingen plattformskonfiguration, ingen tredjepartspanel och inget supportärende.
Prestanda. Direkta PostgreSQL-anslutningar med en finjusterad pool presterar bättre än anslutningar som dirigeras genom en hanterad plattforms infrastruktur. Frågornas svarstid minskade märkbart efter migreringen.
Motprestationen är ansvaret för driften. Vi hanterar databasen, sköter säkerhetskopiering, övervakar replikering och planerar kapacitet. För ett team som är bekvämt med PostgreSQL-drift är detta hanterbart. För ett team som inte är det kan den hanterade plattformens abstraktioner vara värda sina begränsningar.
Atlas Verified körs på PostgreSQL i egen drift med anpassad realtidsinfrastruktur och behandlar tusentals verifieringar av handelsefterlevnad dagligen i den globala leveranskedjan.
