Voordat de factuurknop live ging, bouwden we wat hem tegenhoudt

Het systeem stond op het punt klanten te gaan mailen. Niets hield het tegen om ze allemaal te mailen.

Risico en beheersing
Eén week

Een Nederlandse zonweringinstallateur

Geautomatiseerd factureren was dagen verwijderd van livegang. We stopten en bouwden eerst de beheersmaatregel die de schade begrenst: één trechter waar elk uitgaand bericht doorheen moet, met idempotentie die in de database wordt vastgelegd voordat de provider ooit wordt aangeroepen.

20 naar 1
gelijktijdige aanroepen naar providerverzendingen
5
verzendpunten achter één trechter
2.347
tests geslaagd bij overdracht

Het vraagstuk

Uitgaande e-mail is het punt waarop een softwarefout niet langer intern is. Een bug in een rapport is gênant; een bug in een verzendlus bereikt elke klant die u hebt, permanent, in ongeveer vier seconden.

Het risico was niet hypothetisch. Toen we het draaiende systeem maten, door de live container te lezen in plaats van het configuratiebestand, stond de uitgaande poort al open. Wat klantmail feitelijk tegenhield, was een ontbrekende omgevingsvariabele. Dat is geen beheersmaatregel. Dat is geluk.

De gangbare maatregelen zijn ook precies de maatregelen die falen onder de omstandigheden waarin ze nodig zijn: een retry die dubbel verstuurt, een rate limiter die zichzelf reset, een allowlist die openvalt als de opzoeking faalt.

Wat we deden

01

Eén trechter, geen uitzonderingen

Elk uitgaand pad werd omgezet naar één beveiligde functie, en een test die de broncode scant houdt het aantal verzendpunten vast: er kan er niet stilletjes een bij komen.

02

Eerst vastleggen, dan aanroepen

Een unieke databaseconstraint legt de verzendsleutel vast voordat de e-mailprovider wordt benaderd. Mislukt het vastleggen, dan wordt de provider nooit aangeroepen. Duplicaten worden onmogelijk in plaats van onwaarschijnlijk.

03

Een breaker die zichzelf niet herstelt

Overschrijdingen van de rate limit laten een circuit breaker vallen zonder automatisch herstel. Een mens moet hem vrijgeven en een reden vastleggen. Een breaker die zichzelf reset, spreidt een incident alleen uit over een langer venster.

04

Bij twijfel altijd dicht

Kan de allowlist niet worden geëvalueerd, dan wordt er niets verstuurd. Elke override is mogelijk maar wordt geaudit, zodat het omzeilen van de bewaker een spoor achterlaat.

05

Repeteren tegen het echte schema

De migratie werd gerepeteerd op een wegwerpkopie van het productieschema, en elke constraint werd negatief getest: aantonen dat hij slechte invoer werkelijk weigert, niet alleen dat hij bestaat.

Hoe het werkt

Vaste volgorde

Controles lopen in een vaste volgorde: ongeldig, droogloop, goedkeuring vereist, allowlist, provider uitgeschakeld, breaker. Zo is de reden dat een bericht niet werd verstuurd altijd eenduidig.

Idempotentie op databaseniveau

De uniciteitsgarantie zit in de database, niet in de applicatielogica, zodat die standhoudt over processen, herstarts en gelijktijdige workers heen.

Expliciet inschakelen

Live verzenden vereist een positieve configuratie. Een ontbrekende instelling betekent dat er niets wordt verstuurd, niet dat alles wordt verstuurd.

Geaudite override

Er is een manier om verzending af te dwingen, omdat die er operationeel moet zijn. Die wordt gelogd met de onderbouwing erbij.

Gelijktijdigheid bewezen, niet aangenomen

Twintig gelijktijdige aanroepen om hetzelfde bericht te versturen, leveren precies één provideraanroep op. Dat wordt vastgesteld door een test, niet beargumenteerd in een document.

Rate limits bewezen

Een lus van vijftig pogingen bereikt de provider hooguit tien keer en laat vijftig auditregels achter, zodat zowel de limiet als de logging aantoonbaar zijn.

Constraints negatief getest

Elke constraint werd getest door te proberen hem te schenden. Een constraint die bestaat maar niet weigert, is erger dan geen, omdat hij wordt vertrouwd.

Gaten opgeschreven

Wat bij de overdracht nog niet was gedaan, zoals een migratie die nog niet was toegepast en een live verzending die nog niet had plaatsgevonden, werd expliciet benoemd in plaats van impliciet gelaten.

Technologie

TypeScript PostgreSQL Prisma Next.js Vitest

Het resultaat

Vijf uitgaande verzendpunten werden samengebracht achter één beveiligde trechter, en daar gehouden door een test die faalt zodra er een zesde verschijnt. Vijfendertig nieuwe tests dekken specifiek de bewaker, binnen een suite van 2.347.

De gemeten beginsituatie is het deel dat het onthouden waard is: de poort stond open, en wat e-mail tegenhield was een niet-ingestelde variabele. Het draaiende systeem lezen in plaats van het configuratiebestand is het verschil tussen denken dat u een beheersmaatregel hebt en er een hebben.

Niets hiervan levert een mooie demo op. Het is het werk waardoor de factuurfunctie kan worden aangezet zonder dat iemand zijn adem inhoudt.

Er worden hier geen infrastructuur-ID's, providercredentials, inhoud van de allowlist of omgevingsdetails gepubliceerd.

Verwant werk

Een Nederlandse zonweringinstallateur

408 berichten in een week. Drieëndertig waren echte klanten

Eén gedeelde inbox slokte het hele bedrijf op. Het sorteren ervan was ongemerkt iemands taak geworden.

TypeScript Node.js Google Workspace API
Ons eigen platform

Vijfentwintig containers, twee nodes en een AI-agent met een sleutel tot alles

Een AI productie-infrastructuur laten beheren is roekeloos of goed bestuurd. Het verschil zit volledig in de grenzen.

Proxmox Docker Terraform