Een moderne webapplicatie in C

Alles aan marketyour.games is modern. Het is een webproduct met gebruikersaccounts, een zoekdashboard voor contentmakers, mappen om deze te organiseren, betalingen via Stripe en transactionele e-mails via Postmark. Het volgt het principe van progressive enhancement en vereist geen JavaScript.

Alles is modern, behalve de manier waarop het is gemaakt en hoe het wordt geserveerd. Hier zijn mijn gedachten over het schrijven van een echt product in C en het serveren daarvan via CGI in 2026.

Kort samengevat

De neergang van CGI was gerechtvaardigd in 2004, maar dat argument houdt in 2026 geen stand. Je kunt een moderne webapplicatie schrijven in C. Ik heb het gedaan. Eén binair bestand, een SQLite-bestand, drie afhankelijkheden en het geheel past in je hoofd.

Hoe het is gedaan

De stack is BCHS: OpenBSD, C, httpd en SQLite.

  • Geen framework: Je schrijft een C-programma dat CGI-omgevingsvariabelen leest en reageert.
  • Geen templating-taal: Je schrijft C-code die de HTML direct output.
  • Geen aparte database-server: Je opent het databasebestand via de C-API van SQLite.
  • Geen container: Je bouwt een statisch gelinkt binair bestand en plaatst dit in een map.

Elk verzoek start een vers proces. Het implementeren van een nieuwe versie betekent simpelweg het vervangen van één bestand. Dit betekent geen serverherstart en geen downtime; nieuwe verzoeken maken simpelweg gebruik van het nieuwe binaire bestand.

OpenBSD wordt geleverd met verstandige standaardinstellingen en biedt tools om een C-programma daadwerkelijk te beveiligen, zoals pledge en unveil. httpd serveert standaard vanuit een chroot, waardoor je CGI-binary gesandboxed draait in zijn eigen hoek van het bestandssysteem.

Ik leun op drie aanvullende bibliotheken:

  • libcurl handelt alle uitgaande verzoeken naar Stripe en Postmark af.
  • libsodium regelt het genereren van willekeurige identificatoren, het hashen van wachtwoorden en de HMAC-verificatie voor Stripe-webhooks.
  • libcjson handelt de JSON-parsing voor Stripe en Postmark af.

Slechts twee hiervan zijn daadwerkelijke build-afhankelijkheden: libcurl en libsodium. libcjson is een enkel C-bestand en heeft geen externe afhankelijkheden. SQLite levert een officiële amalgamation (een enkel C-bestand). Beiden zijn vendored en worden als onderdeel van het project gebouwd.

Het resultaat is een build met bijna geen bewegende delen. Een schone build duurt enkele seconden en herbouwen is in minder dan een halve seconde voltooid. In plaats van statische assets op de traditionele manier te serveren, routeer ik al het verkeer van httpd naar de CGI-binary. Elk asset is gecompileerd in het uitvoerbare bestand en wordt vanuit het geheugen geserveerd.

Wat ik waardeerde

Basisprincipes boven frameworks

CGI is geen framework dat je moet leren. De 1.1-specificatie werd gepubliceerd in 2004 en is sindsdien niet veranderd. Er is geen API die je uit je hoofd moet leren. De volledige interface bestaat uit een handvol fundamentele HTTP-concepten:

  • Request headers
  • Request methods
  • Query strings
  • Body data

Als je HTTP begrijpt, begrijp je CGI. Het is onafhankelijk van taal en platform; je leert het één keer en die kennis blijft voor altijd waardevol.

Duidelijke scheiding van verantwoordelijkheden

Een moderne webapplicatie plaatst doorgaans een webserver zoals nginx ervoor, die via een reverse-proxy communiceert met een applicatieserver geschreven in een andere taal zoals Ruby, Python, Go of Rust. Dat voelde voor mij altijd vreemd: je hebt in feite twee webservers, waarvan er één ook verantwoordelijk is voor je businesslogica.

CGI geeft je één webserver die je businesslogica direct aanroept. Het is duidelijk wie wat doet. Dit maakt debuggen ook eenvoudiger. Je applicatie is een gewoon programma dat je vanuit een shell kunt draaien: stuur er een verzoek naar via een pipe, volg stderr, of stap erdoorheen met gdb.

In zekere zin lijkt CGI op serverless architectuur: een supervisor roept je code per verzoek aan, geeft het een set omgevingsvariabelen en verzamelt het resultaat. CGI is serverless, maar dan zonder de vendor (en de verrassingsrekeningen).

Minimaal risico in de supply chain

Met de opkomst van autonome agenten en wekelijkse aanvallen op pakketrepositories is het geruststellend om zeer weinig afhankelijkheden te hebben. De meeste moderne tech-stacks komen met een pakketbeheerder en een bijbehorend ecosysteem. Dat is handig, maar de afhankelijkheidsgrafiek explodeert. Eén of twee directe afhankelijkheden kunnen honderden indirecte metrekken, elk beheerd door iemand van wie je nog nooit hebt gehoord, met inloggegevens die sinds 2015 niet zijn gewijzigd.

Mijn supply chain bestaat uit een C-compiler en drie bibliotheken. Het compromitteren hiervan zou vereisen dat libcurl gecompromitteerd wordt... een project waar het hele internet op let, in plaats van een pakket met elf wekelijkse downloads en een eigenaar die in 2019 is gestopt.

Eenvoudige tooling

Ik ben in mijn hart een minimalist. Ik gebruik klassiek Vim zonder plugins. Dat betekent geen LSP, geen autocomplete, geen codeformatters en geen AI. Complexe tech-stacks vereisen complexe tooling; ze hebben een language server nodig om door de lagen van abstractie te waden. Maar deze stack is eenvoudig genoeg dat een platte editor alles is wat je nodig hebt. De volledige applicatie is een directory met C-bestanden en een Makefile. Open een bestand, lees het, en je weet wat het doet.

Uitstekende tooling binnen handbereik

Wanneer je wel tooling wilt, is die niet ver weg. Sanitizers, valgrind, gdb, ctags, cscope... dit zijn volwassen (sterker nog: geriatrische) tools die decennialang zijn verfijnd en met één pakketinstallatie beschikbaar zijn op elk systeem dat C kan compileren.

Deze volwassenheid gaat verder dan de tools zelf: integratie is ingebouwd in veel editors. Vim wordt geleverd met native ondersteuning voor ctags, cscope en heeft een grafische gdb-frontend (zie :h Termdebug).

HTML en CSS schrijven in C is niet zo slecht

Ik aarzelde om de volledige frontend in C te schrijven, maar het was minder pijnlijk dan ik dacht. Te vaak heb ik moeten vechten met:

  • Vim om de syntax van Go's html/template te highlighten.
  • Helix om Go-templates te detecteren zonder de HTML-detectie te breken.
  • Rustrover om Jinja-templates te herkennen voor een project dat Askama gebruikt.

Het gebruik van C betekent dat er geen tweede taal is die je editor half-detecteert, geen gemengde syntax waarbij de highlighting uit elkaar valt, en geen plugin-dans om de twee talen te laten samenwerken. Het was verfrissend om gewoon C te schrijven. Het is niet mooi, maar dat is Tailwind ook niet.

SQLite is een verademing

Ik heb standaard altijd voor Postgres gekozen voor webapplicaties. Het is een geweldige database, maar het brengt een operationele last met zich mee. Er is een serverproces om in de gaten te houden en frequente releases om naar te upgraden. SQLite heeft niets daarvan. Er is geen proces, geen configuratie, geen afhankelijkheden. Je applicatie opent het databasebestand direct. De operationele last is minimaal.

Wat ik zou veranderen

CGI is een interface tussen een webserver en je applicatie. Er is geen framework of bibliotheek. Dit betekent dat je bijna niets "out of the box" krijgt. Je moet je eigen utilities schrijven voor zaken als:

  • Het parsen en aanmaken van cookies.
  • Het parsen en valideren van query-parameters.
  • Het parsen en valideren van formulierdata.
  • Het escapen van input voor weergave.

Ik heb mijn eigen utilities gemaakt in één headerbestand (cgi.h), maar ik ben er niet overtuigd dat dit de juiste oplossing was. Bibliotheken zoals kcgi bestaan, maar het adopteren daarvan betekent dat je meegaat in hun API en beveiligingsmodel.

Ik zou graag een set gedeelde utilities zien die ontwikkelaars simpelweg kunnen toevoegen (vergelijkbaar met de stb-bibliotheken). Misschien publiceer ik cgi.h als startpunt.

Conclusie

CGI is oud, maar niet vergeten. De meeste redenen waarom we het begin jaren 2000 hebben verlaten, houden vandaag de dag geen stand. Het starten van processen (forking) is niet bijna zo duur als voorheen; moderne kernels optimaliseren dit zwaar. En mochten de kosten van forking ooit een probleem worden, dan is FastCGI een eenvoudige upgrade.

Het schrijven van een moderne webapplicatie in C is meer dan haalbaar — marketyour.games is een commercieel product dat draait op een enkel binair bestand via een serveermodel dat officieel in 2004 is gestandaardiseerd. Het heeft minder bewegende delen dan alles wat ik tot nu toe heb gelanceerd. Ik zal deze stack zeker opnieuw gebruiken.