De stack

Een wiki die niet te redden was, laag voor laag gered

2025 tot 2026 · ongeveer 400 uur · meer dan 20 technische rapporten

Eerst bewezen dat het kon, op een kopie, en pas daarna een offerte.

Dezelfde doorsneden, als tekst

Wat elke laag doet, en wat er speelde

atlasmuseum

  1. MediaWiki

    het fundament

    1.26.4 naar 1.39.10, in drie stappen

    De software onder een openbare atlas van kunst in de openbare ruimte in Frankrijk, voor een vereniging met steun van het Franse Ministerie van Cultuur.

    Het probleem

    De live site was overgenomen door piratenaccounts en draaide op een versie uit 2015 zonder beveiligingsupdates. Een eerdere migratie was halverwege gestaakt en had gemengde en ontbrekende tabellen achtergelaten. Meer dan één partij zei: niet te redden.

    De oplossing

    Eerst een gratis haalbaarheidstest op een lokale kopie, pas daarna een offerte. Daarna in stappen omhoog: 1.31, 1.35 en 1.39.10. Tienduizenden spampagina's en accounts verwijderd, de verwijderlijst drie keer nagelopen.

    Voor developers

    MediaWiki 1.26.4 naar 1.31 naar 1.35 naar 1.39.10, met het actor-model, de Multi Content Revision-tabellen en de extensiemanifesten van 1.39 onderweg.

    bron: opleverdocument atlasmuseum

  2. PHP 8.3

    de motor

    7.4 naar 8.3.36 · 43 extensies mee

    De taal waarin de wiki en al zijn uitbreidingen geschreven zijn. Van versie 7.4 naar 8.3: een nieuwe motor in een auto die blijft rijden.

    Het probleem

    De collectiepagina werkte onder PHP 8 helemaal niet meer: argumenten in de verkeerde volgorde, een paginering die nooit verder kon komen, en één ongeldige opdracht die het hele verzoek liet crashen. In de kaartgenerator zat een SQL-injectie: invoer van buiten kon als opdracht aan de database worden gelezen.

    De oplossing

    De collectiepagina regel voor regel hersteld. Het lek gedicht met een prepared statement, zodat invoer nooit meer als opdracht wordt gelezen. Sinds oktober 2026 draait de motor in een eigen container op mijn eigen server, die ik bij elke beveiligingsupdate in drie minuten van nul opnieuw bouw.

    Voor developers

    collection.php: join() met omgekeerde argumenten, array_merge() op een objectresultaat, een pagineringsplafond van 4.000 onder een paginagrootte van 5.000. map.php::getMap(): SQL-injectie via de parameter nature. Image op php:8.3-fpm-bookworm met mysqli, intl, gd, zip, calendar, exif, opcache en APCu 5.1.28, herbouwd met --pull --no-cache. De versie bovenaan wordt elke tien minuten live uit api.php van de wiki gelezen.

    $stmt->bind_param(str_repeat('s', sizeof($nature)), ...array_values($nature));

    AMAPI, map.php

  3. MySQL 8

    het archief

    5.7 naar 8.0, ter plekke gerepareerd

    Hier staat alles: elke versie van elke pagina. Opnieuw beginnen had de hele geschiedenis gekost.

    Het probleem

    De database zat vol schade van de afgebroken migratie: dubbele revisienummers, alleen al op één revisie meer dan 8.000 rijen, een ontbrekende index en halve tabellen van nieuwere versies.

    De oplossing

    Gerepareerd terwijl alles bleef staan, niet opnieuw opgebouwd. Ruim 8.000 rijen hernummerd, de index en de tabellen aangevuld, zodat de volledige geschiedenis bewaard bleef.

    Voor developers

    Dubbele archive revision ids hernummerd (revisie 149315), ontbrekende revision timestamp-index, actor-migratie, MCR-tabellen, reparaties in change tags, logging, filearchive en user talk.

    bron: opleverdocument atlasmuseum

  4. Semantic MediaWiki

    de catalogus

    51.277 entiteiten, 0 overgeslagen

    De laag die weet welke kunstenaar welk werk maakte en waar het staat. Daarmee zoekt en filtert de atlas.

    Het probleem

    Een onderdeel werd niet geladen. Daardoor stond op 4.589 pagina's bij de kunstenaar niet diens naam, maar de naam van een variabele uit de code.

    De oplossing

    De oorzaak gevonden, het onderdeel geladen en de hele catalogus opnieuw opgebouwd: 51.277 entiteiten in negen en een halve minuut, nul overgeslagen, afgesloten met exit=0.

    Voor developers

    PageForms was niet geladen, dus #arraymap klapte nooit uit en SMW sloeg de lusvariabele op (Auteur=Artist) in plaats van de waarde. rebuildData.php onder SMW 5.1.0.

    $ php extensions/SemanticMediaWiki/maintenance/rebuildData.php --auto-recovery

    wiki139/logs/rebuildData-20260909-015400.log

  5. Cache in twee lagen

    de snelheid

    26 tot 64 keer sneller

    Wat al eens is opgezocht, wordt onthouden. De bezoeker wacht niet opnieuw op iets dat het systeem al weet.

    Het probleem

    De site riep zichzelf via een omweg aan, en externe bronnen zoals Wikidata werden zonder cache bevraagd. Pagina's waren daardoor traag.

    De oplossing

    De omweg weggehaald, een cache in twee lagen rond de externe aanroepen, de paginacache aan met een epoch om gericht te verversen, en afbeeldingen die pas laden als ze in beeld komen. Herhaalde paginaladingen 26 tot 64 keer sneller, gemeten.

    Voor developers

    Memoisatie binnen één verzoek plus een schijfcache met een TTL van dertig minuten rond de upstream API en SPARQL; parser- en paginacache met cache-epoch; server-to-self loopback verwijderd; galerijen achter een IntersectionObserver.

    bron: opleverdocument atlasmuseum

Uitvaart-SaaS

  1. Next.js en React

    24 schermen

    4.000 dossiers: 2.100 naar 40 ms

    Alles waar een uitvaartondernemer in werkt: het dossier met zeven tabbladen, een weekkalender, vervoer, het draaiboek van de uitvaartdag, inkoop en facturen.

    Het probleem

    Met 4.000 dossiers in het systeem deden de overzichtsschermen er ongeveer 2.100 milliseconden over.

    De oplossing

    Paginering en de juiste indexen in de database. Dezelfde schermen laden nu in ongeveer 40 milliseconden, gemeten op 4.000 dossiers.

    Voor developers

    Lijstschermen van ongeveer 2.100 ms naar ongeveer 40 ms op 4.000 dossiers, door paginering en indexering. Vier productie-dependencies: next, react, react-dom en postgres. Geen ORM, geen CSS-framework, geen authenticatiebibliotheek.

    bron: overdracht september 2026

  2. Wetgeving

    Wet op de lijkbezorging

    termijnen in de database

    Een uitvaart moet aan strikte wettelijke termijnen voldoen. Het systeem bewaakt die termijnen zelf, in de database, in plaats van te vertrouwen op wie de planning maakt.

    Het probleem

    De wet kent harde termijnen: niet eerder dan 36 uur na het overlijden, uiterlijk de zesde werkdag, aparte termijnen voor as, verlofregels, btw-regels, en facturen die na verzenden niet meer mogen veranderen.

    De oplossing

    Die regels staan in de database zelf in plaats van verspreid door de applicatie. Elk scherm, elke import en elke toekomstige koppeling loopt langs dezelfde regels, op dezelfde plek.

    Voor developers

    De 36-uurstermijn en de zesde werkdag uit de Wet op de lijkbezorging, verlofregels, de termijn uit artikel 59 voor as, btw-regels en factuurvergrendeling, vastgelegd in de database in plaats van in applicatiecode.

    bron: overdracht september 2026

  3. PostgreSQL

    plain SQL

    46 tabellen · 119 SQL-tests

    De gegevens van alle uitvaartondernemers. De database test zichzelf: 119 tests controleren of de regels doen wat ze moeten doen.

    Het probleem

    Als de regels in de database zitten, moeten ze ook daar getest worden. Een test in de applicatie bewijst niets over een regel die in de database draait.

    De oplossing

    119 tests die rechtstreeks in de database draaien, over 46 tabellen en 29 genummerde migraties.

    Voor developers

    29 SQL-migraties, 46 tabellen, 119 SQL-tests, ongeveer 19.300 regels code bij de overdracht.

    bron: overdracht september 2026

  4. Row-level security

    multi-tenant

    FORCE en een RESTRICTIVE rolslot

    Alle uitvaartondernemers werken in hetzelfde systeem, elk op een eigen subdomein. Ieder heeft een eigen kluis in hetzelfde gebouw.

    Het probleem

    In software voor meerdere uitvaartondernemers is één vergeten filter in een query genoeg om dossiers van een andere klant te tonen.

    De oplossing

    Row-level security op elke tabel, met FORCE en een RESTRICTIVE rolslot. Een query die zijn filter vergeet, krijgt niets terug in plaats van andermans dossiers. De database beschermt, niet de discipline van de programmeur.

    Voor developers

    RLS op elke tabel met FORCE ROW LEVEL SECURITY en een RESTRICTIVE policy op de applicatierol, zodat een query zonder tenantfilter een lege set geeft.

    bron: overdracht september 2026

  5. Auditlog

    alleen toevoegen

    oude en nieuwe waarde, altijd

    Alles wat er in een dossier verandert, wordt vastgelegd met wat er stond en wat het werd. Daarmee is een vergissing terug te draaien.

    Het probleem

    Bij uitvaartdossiers moet elke wijziging herleidbaar zijn, en een vergissing van een klant moet te herstellen zijn.

    De oplossing

    Een auditlog waar alleen aan kan worden toegevoegd, met de oude en de nieuwe waarde van elke wijziging. Hetzelfde log drijft de functie ongedaan maken.

    Voor developers

    Append-only auditlog met oude en nieuwe waarden; de undo-functie leest uit hetzelfde log.

    bron: overdracht september 2026

  6. Datacenter, Naaldwijk

    de hosting

    ISO 27001 · ISO 9001 · ISO 14001 · NEN 7510

    De servers staan in een Nederlands datacenter waarvan de beveiliging elk jaar door een onafhankelijke partij wordt gecontroleerd.

    Het probleem

    Uitvaartdossiers bevatten persoonsgegevens van overledenen en nabestaanden. Een klant wil weten waar die staan en wie daarop let.

    De oplossing

    Lancering in een datacenter in Naaldwijk dat gecertificeerd is volgens ISO 27001, ISO 9001, ISO 14001 en NEN 7510. Dat zijn certificeringen van het datacenter, niet van de software zelf.

    Voor developers

    Het datacenter heeft ISO 27001 (informatiebeveiliging), ISO 9001 (kwaliteit), ISO 14001 (milieu), NEN 7510 (informatiebeveiliging in de zorg), SOC 1 en SOC 2, en PCI DSS. De software zelf scheidt klanten met row-level security in de database.

    bron: certificeringen van het datacenter, opgehaald 17 oktober 2026

Deze site

  1. React

    de interface

    werkt ook zonder JavaScript

    Het scherm waarop een bezoeker typt en klikt. Het contactformulier werkt ook zonder JavaScript, dus er valt niemand af.

    Het probleem

    Op een iPhone was de pagina 16 pixels breder dan het scherm. Safari zoomde daardoor uit, de pagina schoof opzij en rechts ontstond een donkere strook.

    De oplossing

    De oorzaak gevonden in een menurij en een vaste achtergrond die over de rand staken. Opgelost, en de hele site kreeg overflow-x: clip als vangnet, zonder de sticky scènes te breken.

    Voor developers

    Het formulier is een echte form met een echte action; de client-state toont alleen de voortgang. overflow-x: clip in plaats van hidden, omdat hidden de root tot scrollcontainer maakt en position: sticky breekt.

    const [submitting, setSubmitting] = useState(false);

    apps/web/src/components/contact-form.tsx:40

  2. Next.js

    App Router

    10 pagina's · 3 API-routes

    Als de receptie van een kantoor: neemt het bericht aan, kijkt of alles is ingevuld en bevestigt meteen.

    Het probleem

    De build op de server faalde zodra Google Fonts even niet bereikbaar was. De site kon dan niet worden bijgewerkt omdat een andere dienst haperde.

    De oplossing

    De lettertypes meegeleverd in plaats van bij Google opgehaald. De build heeft geen externe dienst meer nodig.

    Voor developers

    next/font/google vervangen door next/font/local met de woff2-bestanden en hun OFL-licenties in de repo, zodat de build hermetisch is.

    export async function POST(request: Request): Promise<Response> {

    apps/web/src/app/api/v1/enquiries/route.ts:55

  3. Node.js

    de worker

    nooit twee keer dezelfde rij

    Twee postbodes werken tegelijk en pakken nooit dezelfde brief. Er gaat geen bericht verloren en er komt er geen dubbel aan.

    Het probleem

    Twee workers draaien tegelijk. Zonder afspraak kunnen ze hetzelfde bericht pakken, of halverwege stoppen en het bericht laten liggen.

    De oplossing

    Elke worker claimt een rij en neemt hem vijf minuten in huur. Stopt hij onderweg, dan komt de rij daarna vanzelf weer vrij.

    Voor developers

    UPDATE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED) met een lease op notify_after van vijf minuten, in één statement.

    FOR UPDATE SKIP LOCKED

    packages/db/src/enquiries.ts:108

  4. PostgreSQL

    17 · plain SQL

    4 migraties · geen ORM

    Het archief waar alles in staat. Elke wijziging is vastgelegd als een genummerd bestand, zoals notulen.

    Het probleem

    Als de mailserver plat ligt, mag geen aanvraag verloren gaan, en de bezoeker mag er niets van merken.

    De oplossing

    De aanvraag wordt eerst opgeslagen; mailen is een losse stap erna. Mislukt die, dan volgt een nieuwe poging na één minuut, steeds dubbel zo lang tot zes uur, maximaal twaalf keer, met de foutmelding bij de rij.

    Voor developers

    Transactionele outbox op één tabel: notify_attempts, notify_after en notify_error, met een backoff die bij één minuut begint en verdubbelt tot zes uur.

    ADD COLUMN notify_attempts integer     NOT NULL DEFAULT 0,

    packages/db/migrations/0004_enquiry_notification.sql:12

  5. Hashketen

    het bewijs

    elk record vast aan het vorige

    Elke meting van deze site krijgt een zegel dat aan de vorige vastzit. Wie er later iets aan verandert, breekt de keten, en dat kunt u in uw eigen browser nakijken.

    Het probleem

    Een getal op een website is niets waard als de eigenaar het achteraf stil kan aanpassen.

    De oplossing

    Elke meting wordt gehasht samen met de hash van de vorige. Eén gewijzigd record maakt alle volgende hashes ongeldig, op een aanwijsbare plek.

    Voor developers

    hash = sha256(utf8(prev_hash_hex) || utf8(canonical_json(body))), met canonieke JSON volgens RFC 8785, zodat dezelfde meting altijd dezelfde hash geeft.

    return Sha256.unsafe(sha256Hex(concatBytes(utf8(prevHash), utf8(canonical))));

    packages/core/src/chain/hash-chain.ts:88

Logo's: Simple Icons (CC0), naar de merkpagina's van de eigenaars. De overige tekens zijn eigen werk.