Infrastruktur Del 2: Varje måndag 06:00. 100 % byggt med AI.

I juli 2026 hade vi tröttnat på WordPress. Vid det laget hade Johans nyhetsbrev Tech Insights gått ut varje måndag klockan 06:00 i två år i rad, till drygt 3 000 prenumeranter. Idag sköts utskicken i stället av vår egen motor. En lösning som läser handskriven markdown, bygger webben och mejlet från samma källa, och avfyrar alltihop i ett enda API-anrop.

I vår förra artikel berättade vi hur huvudwebben lämnade WordPress. Tech Insights gjorde samma resa veckan därpå. Den här texten handlar om den andra halvan av flytten – den där misstag faktiskt landar i någons inkorg. Det här är berättelsen om hur vi byggde upp en prenumerantdatabas, hur vi hanterar samtycke, och hur vi tvingade fram ett system som skickar mejlet exakt en gång. Helt utan skyddsnät.

Prenumeranter, mejl och LinkedIn
3 000 +
Nummer hittills
106
Commits på sex dagar
102
Säkerhetstester
78
API-anrop per utskick
1
Spårningspixlar
0

01

Sviten som inte får brytas

Tech Insights drog igång sommaren 2024. Som de flesta nyhetsbrev på egen domän började det som en WordPress-sajt med ett tillägg för utskick. Det fungerade, och det kostade pengar. Årsavgifter för tillägg som skötte backuper, SEO, filuppladdningar och själva mejlen. Varenda vecka upprepades samma manuella rutin: skapa ett inlägg i adminpanelen, klistra in texten från markdownfilerna, se en bilduppladdning krascha, ladda upp bilden en gång till, och sedan trycka på skicka.

Den rutinen höll i två år. När vi började bygga om var sviten uppe i 105 nummer – ett varje vecka sedan juli 2024. Hela nyhetsbrevet vilar på den sviten. Det bygger förväntningar. Det som skulle ersätta WordPress var tvunget att leverera på måndag. Varje måndag, utan undantag.

Varje ruta är ett nummer i sin kalendervecka, juli 2024 till juli 2026. Den orange rutan är vecka 30: det första numret som den nya motorn skickade.
Varje ruta är ett nummer i sin kalendervecka, juli 2024 till juli 2026. Den orange rutan är vecka 30: det första numret som den nya motorn skickade.

I juli såg vi en väg ut. Systersajten tokentek.ai lämnade WordPress för Astro först (den flytten är ett eget case), och veckan efter hakade den här sajten på. Statiskt bygge, innehållet i git, och en server som drar hem koden och publicerar sig själv. Men webbplatsen var den enkla biten för Tech Insights. En trasig hemsida kan byggas om och publiceras på ett par minuter; på tisdag har alla glömt det. Den knepiga halvan var prenumerantlistan och utskicket. Den delen har en egenskap som webben saknar: det viktigaste den gör kan inte göras ogjort.

02

Skickat är skickat

Gör du bort dig på webben publicerar du bara om sidan, så är misstaget borta. Ett utskick som går snett går inte att rädda. Skickar du två gånger får hela listan en dubblett i inkorgen. Skickar du till någon som avregistrerat sig bryter du ett löfte, och lagen har synpunkter. Skickar du till halva listan har du sedan inget pålitligt sätt att nå den andra halvan. Och läcker nästa veckas nummer på sajten redan under lördagen, är utskicket förstört innan det ens har skett.

Därför byggde vi motorn baklänges. Vi utgick från det värsta som kunde hända. Allt fram till utskicket måste gå att göra om: importen, bygget, förhandsvisningen, testmejlet, till och med beslutet att skicka. Endast en enda gräns är oåterkallelig: sekunden då Postmark – tjänsten som pratar med världens mejlservrar – accepterar utskicket. Fram till dess kan vi backa bandet. Efter det får ingenting skicka det numret igen.

Att dra den gränsen rätt tog oss sex dagar. Den första versionen stod klar på en dag.

03

En motor på en dag

Den första ändringen i kodbasen gjordes onsdagen den 15 juli klockan 10:14. Den skrevs av Claude Fable 5 i Claude Code. Den innehöll Astro-sajten, alla äldre nummer konverterade från Obsidian med optimerade bilder, en spärr som dolde framtida inlägg, en renderare för mejlen, PHP-kod för anmälan med dubbel bekräftelse och ett skript för uppdatering som följde samma mönster som systersajten. Senare samma kväll var avregistreringen klar. Vi hade ett nyhetsflöde som systersajten kunde läsa, och en synk av Postmarks spärrlista för adresser som studsat eller markerat mejlet som skräppost. Femton commits, dag ett.

När grunden var på plats satte vi GPT-5.6 i Codex på att granska säkerheten. Först en bred genomgång, sedan flera omgångar av rättningar: först tretton, sedan nio, sedan sju. Varje rättning täppte till en lucka där två samtidiga händelser kunde krocka, vilket hade kunnat leda till att en läsare fick fel innehåll.

Ett bygge mitt i ett utskick kunde till exempel para ihop ämnesraden från en äldre utkastversion med texten från en nyare. Vi återskapade den värsta luckan med en testlista. Ett avbrutet utskick återupptogs precis efter att sajten hade byggts om. Resultatet? Samma utskick skickades i två versioner. Femhundra testadresser fick ämne och text A, nummer femhundraett fick ämne och text B. Systemet bokförde utskicket som en fullständig framgång. Ingen riktig läsare märkte något, men testerna visade att våra rättningar bara krympte luckorna.

Efter två dagar insåg vi att det inte handlade om att lappa koden längre. Vi var tvungna att bygga på en mycket stadigare grund för att helt eliminera risken för dessa krockar.

04

En fil äger sanningen

Den 17 juli introducerade vi en SQLite-databas som kärnan i systemet. En enda fil. Den går att läsa från både PHP och Node, den hanterar transaktioner, och den låser sig själv. Databasen ligger utanför mappen som besökarna kommer åt, med rättigheter som bara ägaren har. Båda miljöerna vägrar att starta om filrättigheterna är fel.

Allt som nyhetsbrevet behöver minnas bor där: prenumeranter, bekräftelsenycklar som bara sparas som hashar, räknare som stoppar missbruk, status för utskicken och historiken över varje nummer som någonsin skickats. Här sparas också varje uppdatering Postmark skickar om en adress: en avregistrering, ett klagomål på skräppost, eller en adress som slutat fungera. Allt det där tas emot via en enda säker ingång. Databasens uppdateringar är korta, och ingen skrivning hålls öppen medan vi väntar på nätverket. Databasen låser sig alltså aldrig på grund av en trög uppkoppling.

Vem som får säga vad: samtycket bor i SQLite, leveransbarheten hos Postmark, och ett nummer går bara till de adresser där båda säger ja.
Vem som får säga vad: samtycket bor i SQLite, leveransbarheten hos Postmark, och ett nummer går bara till de adresser där båda säger ja.

Designens viktigaste detalj är en maktdelning. SQLite bestämmer över samtycket: vill personen ha mejlet? Postmark bestämmer över leveransen: går det ens att skicka dit? En prenumerant får nyhetsbrevet bara när båda säger ja. Ingen av dem får skriva över den andras beslut. Om en adress slutar fungera permanent (en "hard bounce") stoppar det leveransen, men det ändrar inte på samtycket. En gammal bekräftelselänk kan aldrig upphäva en färsk avregistrering. Varje dygn jämför ett skript de två, städar bort utgångna inbjudningar, kollar databasens hälsa och sparar en verifierad backup. Vi behåller 35 dagliga kopior, och en i månaden under ett år.

Samtycket har också en ytterdörr. Vi gav den samma omsorg.

05

Två steg in, ett klick ut

Att börja prenumerera kräver två steg: du fyller i din adress på webben och klickar på en länk i ett mejl. Själva klicket startar prenumerationen – ingen extra sida, ingen extra bekräftelseknapp. Vi lade mycket energi på säkerheten kring den länken. Den är signerad, den fungerar bara för ett enda klick, den dör efter 48 timmar, och servern sparar bara ett avtryck (en hash) av nyckeln. Inte ens om databasen läcker ut går det att fejka en bekräftelse. Om ett företags antivirus skannar länken ser det en giltig sida, men det startar inte prenumerationen. Bara ett riktigt besök från en webbläsare gör det.

Vårt system svarar exakt likadant oavsett om adressen är ny, om den redan finns, eller om den är ogiltig. Det går alltså inte att fiska efter vilka som prenumererar. Fält som robotar fyller i men människor inte ser (honeypots) och frekvensspärrar för både adresser och IP-nummer håller skräpet borta.

Att lämna kräver däremot bara ett klick. Längst ner i varje mejl finns en länk från Postmark. Ett klick på den avslutar prenumerationen direkt. Ingen inloggning, inget extramoment. Asymmetrin är poängen: två steg in för att slippa misstag, ett klick ut för att den som vill lämna redan har bestämt sig. Motorns enda jobb är att notera beslutet för evigt och aldrig argumentera emot.

När vi flyttade listan från det gamla WordPress-tillägget använde vi samma logik i importen. Den väckte ingen som hade avregistrerat sig, och om importen stötte på en trasig adress stannade den helt istället för att bara läsa in halva listan. Dörrarna var byggda. Kvar var själva innehållet, och ögonblicket då vi skickar iväg det.

06

Bokför först, skicka sedan

Det som landar i inkorgen byggs samtidigt som webbplatsen, och från samma textfil. Vår kod skapar ett rent, enspaltigt HTML-dokument som fungerar i e-postklienter. All design är inbakad direkt i elementen (inlinad CSS), och vi skickar med en version i ren text. Vi tvingar inte fram någon bakgrundsfärg eller textfärg, vilket gör att mottagarens eget mörka läge fungerar precis som de är vana vid. Om texten är för lång bryts den av i slutet och pekar vidare till webben, så att mejlet håller sig under 90 kilobyte. Gmail klipper nämligen allt över ungefär 102 kilobyte, vilket riskerar att dölja avregistreringslänken i botten. Vi har noll spårning i mejlet: inga dolda pixlar, inga spårningslänkar. Motorn vet vad Postmark har levererat, men inte ett skvatt om vad mottagarna gör därefter.

Det är i skickandet som vi byggde bort risken för fel. När utskicket förbereds sparas ett fingeravtryck (en hash) av exakt den HTML och text som ska skickas iväg. Innan sändaren startar kollar den hashen. Sedan bokför den avsikten att skicka i databasen: vilket nummer, innehållet, antalet mottagare, och ett kvitto på själva listan. Först därefter skickar den. Ett enda, massivt anrop till Postmarks Bulk-API, som innehåller själva mejlet och listan med alla mottagare. Ingen loop som skickar en liten del i taget. Ingen tillfällig fil som minns var ett kraschat program stannade.

Antingen tar Postmark emot anropet och vi bevakar det via ett ID. Eller så misslyckas det på ett av två sätt. Får vi ett blankt nej kan vi lösa problemet och be systemet försöka igen, men det kräver att en människa trycker på knappen. Blir svaret oklart – som vid ett serverfel eller om nätet går ner – stannar alltihop. Hela skickandet fryser tills någon kollar databasen mot Postmarks loggar. Verktyget vi byggde för att stämma av det där är extremt pedantiskt. Det kollar anropets ID, ämnesrad, antalet mottagare och tidpunkten innan det accepterar Postmarks status som sanning.

Sändaren i den ursprungliga designen skickade listan i omgångar: varje pil är en fas där en lucka kunde uppstå. Den ombyggda sändaren bokför ett enda oföränderligt anrop före all nätverkstrafik och läser av läget först när anropet accepterats.
Sändaren i den ursprungliga designen skickade listan i omgångar: varje pil är en fas där en lucka kunde uppstå. Den ombyggda sändaren bokför ett enda oföränderligt anrop före all nätverkstrafik och läser av läget först när anropet accepterats.

Det finns inget "skicka direkt"-kommando. Databasen tillåter bara att ett (1) utskick av samma nummer körs. Vi lagade inte bara de luckor vi hittade – vi byggde bort dem strukturellt så att de inte längre kunde uppstå.

07

En simulerad Postmark att öva mot

Man testar inte sådant här mot riktig e-post. I stället sätter våra tester upp en helt egen miljö för varje körning: en tillfällig mapp som raderas efteråt, en lokal databas, ett färdigbyggt utkast och en låtsasversion av Postmarks server. Fyra program körs samtidigt mot samma databas för att säkerställa att databaslåsen faktiskt gör sitt jobb under press. Testerna nuddar aldrig våra riktiga servrar eller lösenord. De körs i CI-miljön vid varje kodändring, och en gång till på servern precis innan vi driftsätter.

Sviten innehåller 78 testfall. De är mer som brandövningar än vanliga enhetstester. Vi simulerar att två personer klickar på en bekräftelselänk exakt samtidigt – nyckeln aktiveras bara en gång. Vi testar vad som händer om statusmeddelanden från Postmark kommer i fel ordning, så att inte ett gammalt beslut skriver över ett nytt. Vi bevisar att när ett nummer väl har godkänts för utskick, skickas exakt de bytes som granskades – även om källkoden har ändrats efter det. En halvfärdig anmälan kastas inte bort förrän tiden faktiskt har gått ut. Och felet som skickade två olika texter till samma lista? Det finns permanent kvar i testerna, bara för att bevisa att det aldrig kan hända igen.

Det är de övningarna som gjorde det möjligt för oss att trycka ihop en hel veckas manuellt jobb till några få tangenttryck.

08

Skarpladdat

Söndagsrutinen styrs nu via en terminal som vi startar med två bokstäver: tj. Panelen jämför koden vi har med det senaste utkastet från Johans Obsidian. Den visar versionerna lokalt, på GitHub och på servern, och den uppdateras var tionde sekund. Val som inte går att göra tonas ner med en tydlig förklaring, så man slipper gissa varför.

[i] import the newest issue from the vault, commit only that issue, and upload it
[l] build and toggle the background local preview
[t] send the checked candidate to one remembered test address
[a] arm/disarm the checked revision for Monday 06:00
[u] unsubscribe one reader locally and at Postmark
[c] copy the current issue as rich HTML for LinkedIn

Så här ser numera en söndagskväll ut. Johan skriver klart texten i Obsidian. Han trycker [i]. Maskinen importerar texten, krymper bilderna, sparar ändringen, skickar den till servern och väntar på kvitto. Utkastet ligger nu på servern, granskat och klart, men dolt för omvärlden. [l] öppnar en förhandsvisning på skärmen. [t] skjuter iväg ett testmejl till en sparad adress – exakt den kod som ska ut, via samma servrar, fast utan att bokföras i databasen.

Sedan kommer det avgörande steget. Johan trycker [a]. Kommandot armerar versionen – och dess unika hash – för måndag 06:00. Det vi laddar maskinen med är bokstavligen de bytes som ska iväg. Om någonting ändras innan måndag, till exempel om vi rättar ett stavfel och laddar upp på nytt, avväpnar maskinen sig själv. Inget skickas förrän Johan aktivt har valt att armera den nya versionen. Ett extra tryck på [a] avväpnar utskicket. Och maskinen kan inte glömma bort det hela: om klockan slår 06:00 och tre timmar går utan att ett utskick har gått iväg, då går larmet.

Johan stänger locket. Ett utkast är armerat. Resten sköter maskinen.

09

Måndag 06:00

Klockan 06:00 vaknar servern. Först görs uppdateringen: testerna körs och sajten byggs två gånger. Det dolda bygget innehåller det nya numret och mejlen. Det publika bygget visar bara historiken över tidigare nummer. Spärren som dolde texter med framtida datum existerar inte längre – den är ersatt av en regel som bara låter utskicket självt bestämma vad som är synligt. Risken för att sajten läcker innehållet för tidigt finns inte, eftersom filen helt enkelt inte ligger i den publika versionen.

Sedan kliver sändaren in. Den kollar att koden den kör är exakt den version som Johan armerade i går. Den stämmer av listan mot Postmark en sista gång, rensar bort alla som hoppat av, bokför beslutet att skicka och avfyrar API-anropet. När Postmark säger "ja", noterar databasen att numret är skickat. Sajten byggs om på fem sekunder och publiceras. Först när texterna ligger uppe på nätet börjar sändaren bevaka leveransen. Den som öppnar mejlet 06:01 klickar på en länk till en sajt som redan fungerar.

Måndagsmorgon utan mänsklig inblandning: kedjan från driftsättning klockan 06:00 via accepterat utskick till publicerad webbplats, och de tre timmarna av återhämtning som följer.
Måndagsmorgon utan mänsklig inblandning: kedjan från driftsättning klockan 06:00 via accepterat utskick till publicerad webbplats, och de tre timmarna av återhämtning som följer.

Går något snett stannar processen. Ett larm mejlas ut och den gamla sajten ligger kvar. I tre timmar därefter går en rutinkontroll var femtonde minut. Den kan laga en avbruten publicering eller återuppta bevakningen av ett utskick, men själva systemet hindrar den från att råka skicka ut dubbla mejl. Misslyckade utskick startas bara om när en människa trycker på knappen, och fastnar det helt står allt stilla tills vi har tittat på det.

Efter frukost gör Johan en sista sak: [c] kopierar hela numret som HTML med bilderna inbäddade, färdigt att klistras in på LinkedIn.

Den första morgonen maskinen fick sköta sig själv helt utan övervakning var den 20 juli.

10

Tre måndagar senare

Mejlet vi armerade den där första söndagen inleddes med orden "Goodbye Wordpress!". Det första maskinen gjorde var alltså att vinka hejdå till sin egen föregångare. Klockan 06:00 uppdaterades servern, utskicket accepterades, webben uppdaterades och hela listan fick numret i inkorgen. Sedan åkte det upp på LinkedIn. Vecka 30 ligger i arkivet.

Det är tre veckor sedan nu. Två måndagar till har passerat utan ett enda gupp. Människans roll har krympt till det den egentligen alltid borde ha varit: skriv texten, tryck på fyra tangenter, gå och lägg dig. Den årliga prenumerationen på WordPress-tilläggen är uppsagd. Den timslånga rutinen av manuell handpåläggning är borta. Kvar är kostnaden per mejl hos Postmark, och en liten kodbas som vi äger från ax till limpa. Vi lät Claude Fable 5 skriva motorn. Vi lät GPT-5.6 granska säkerheten. Vi stresstestade allt mot en lokal fejkserver. Och nyhetsbrevet skrivs fortfarande för hand.

Det är en modell som fungerar på alla processer där det kritiska ögonblicket inte går att ångra. Faktureringar, kundutskick, lönekörningar. Låt en enda plats äga sanningen (databasen). Gör armeringen till ett aktivt beslut knutet till exakta bytes. Bokför avsikten innan du tar klivet ut på nätverket, och stoppa maskinen vid minsta tveksamhet i stället för att chansa. Testa misstagen på en klon. Då är måndagarna tråkiga, precis som de ska vara.

Har ni en process som måste köras exakt en gång?

Motorn som driver Tech Insights byggdes på sex dagar. Vi granskade den tills alla luckor var stängda. Vi designar och bygger maskineri kring oåterkalleliga processer: med tydligt ägarskap, stenhård armering och spärrar som drar i nödbromsen när något ser fel ut. Har er organisation en egen måndag 06:00? Hör av er.