De functionaliteit die niemand vroeg en al het werk deed
Konvoi is een platform voor wagenparkbeheer. Voertuigen, chauffeurs, keuringen, en de Belgische specialiteit: verkeersboetes, die als PDF binnenkomen van een tiental instanties in een tiental layouts en gekoppeld moeten worden aan wie er effectief achter het stuur zat.
Tijdens het bouwen viel op waar het werk zat. Niet in de wagenparklogica. Niet in het matchen van chauffeurs. Het werk zat in het überhaupt binnenkrijgen van documenten: de scanner in het atelier die naar een vast adres mailt, de boekhouder die een pak PDF’s doorstuurt, de instantie die een scan opstuurt, het zipbestand dat iemand erin dropt omdat dat makkelijker was.
Toen begonnen we aan een tweede project en schreven we het opnieuw. Daarna een derde.
Op dat punt aanvaard je ofwel dat je nu drie lichtjes verschillende instroompijplijnen onderhoudt, ofwel geef je toe dat die pijplijn eigenlijk het product was. Wij kozen het tweede en haalden ze eruit als Sluice.
De stelling
Kijk naar zowat elke bedrijfsapplicatie en er staat een document vooraan. Facturen voor boekhouding. Leveringsbons voor voorraad. Boetes voor wagenpark. Voorschriften voor zorg. Contracten voor alles juridisch. Offertes, bestelbonnen, keuringsverslagen, uurbriefjes, douanepapieren.
Vóór het systeem ligt er altijd een stapel papier, en iemand moet die binnenkrijgen.
De applicatie is het interessante deel op het organigram. De instroom is waar het werk zit, en net dat deel staat nooit in de raming. Iemand denkt altijd dat het “gewoon die PDF uitlezen” is.
Het is niet gewoon die PDF uitlezen. Het is: de PDF komt binnen langs zes verschillende deuren, de helft daarvan zijn scans van scans, één deur is een printer uit 2014, één is een mailbox met vier jaar geschiedenis, hetzelfde document komt twee keer per week binnen, en als het misloopt moet iemand het origineel kunnen zien - het échte origineel, niet een versie die iets behulpzaam opnieuw heeft gegenereerd.
Eén functie, alles gaat erdoor
De architecturale beslissing die Sluice doet werken is saai en draagt alles. Elk instroomkanaal loopt door één enkele functie die een buffer aanneemt.
Wat de deur ook is - een webhook, een upload in de browser, een bestand via SFTP, een mailbijlage - het eindigt bij dezelfde naad. Die aanroep schrijft de ruwe bytes naar onveranderlijke opslag, hasht ze, ontdubbelt, maakt een documentrij met volledige herkomst, en zet de verwerking in de wachtrij.
Alles stroomafwaarts is identiek, ongeacht hoe het document binnenkwam. Elk nieuw kanaal is een kleine adapter zonder eigen pijplijn.
Discipline rond die naad is het verschil tussen een platform en zes pijplijnen die samen doen alsof ze een platform zijn.
De originele bytes zijn heilig
Ruwe bestanden landen op een pad dat door de inhoud bepaald wordt, nog voor iets ze aanraakt: workspace, dan jaar, maand, dag, dan de SHA-256-hash en de extensie. Nooit overschreven. Afgeleide zaken - de PDF na OCR, thumbnails, geëxtraheerde JSON, exports - staan onder aparte prefixen.
Die regel bestaat omdat we ze zelf gebroken hebben. In Konvoi schreef de OCR-stap zijn resultaat over het origineel heen. Dat werkte prima, tot iemand wou zien wat de instantie effectief gestuurd had, en wat terugkwam onze eigen weergave ervan was. Alles zag er aannemelijk uit. Niets was nog bewijs.
Bewaar wat er effectief binnenkwam. Kopieën zijn nuttig, maar alleen het origineel beslecht een discussie.
Voor een documentplatform is “we hebben nog exact wat je ons stuurde, byte voor byte” geen extraatje. Het is grotendeels de reden om het ding te vertrouwen. Dus is het nu een structurele eigenschap in plaats van een gewoonte: de ruwe schrijfactie gebeurt vóór de verwerking, en geen enkel codepad overschrijft die sleutel.
Ontdubbelen komt gratis uit diezelfde hash. Dezelfde boete, doorgestuurd door de boekhouder, de bureauverantwoordelijke én de chauffeur, is één document met drie aankomsten.
Zes deuren, één kamer
De kanalen die er vandaag zijn:
Webhook. Een bearer token per credential, één endpoint, duw erin wat je hebt.
Upload in de browser. Bestanden erin slepen, zipbestanden inbegrepen, die uitgepakt worden met limieten op aantal en grootte en zonder geneste archieven.
SFTP, FTP en SMTP. Echte servers op eigen poorten, met eigen inloggegevens. Dit is het onsexy kanaal en het is het kanaal dat dossiers binnenhaalt. Multifunctionele printers spreken geen REST. Ze spreken scan-naar-map en scan-naar-mail, ze zijn in 2016 geconfigureerd, en niemand vervangt ze omdat jij een mooie API gebouwd hebt.
Mail inbound. Handtekening geverifieerd, meerdere domeinen, gerouteerd op ontvangeradres, zodat een workspace facturen@ en boetes@ als aparte instroomadressen kan uitdelen.
Bevraagde mailboxen. Gmail, Outlook en gewone IMAP. Eén gekoppelde account kan meerdere streams per map dragen, wat telt omdat de echte vraag nooit “lees mijn mailbox in” is, maar “lees die map in en blijf van de rest”.
De Belgische E-box is de volgende, want in België is officiële post steeds vaker een overheidsbus in plaats van een enveloppe.
E-mails zijn geen bijlagen
Eén modelleerkeuze bewijst zichzelf telkens opnieuw. Elk document hoort bij een instroomtransactie in plaats van los rond te zweven.
Eén e-mail is één transactie, met onderwerp, tekst en afzender, en de bijlagen als documenten. Eén uploadbatch is één transactie. Een zipbestand is één transactie. Containers - .eml en .zip - worden uitgepakt en nooit zelf als document bewaard, al gaat de ruwe container wel onveranderlijk in back-up.
Dat geeft de classificatie context die geen enkele parser met één losse PDF kan hebben. Een scan zonder hoofding en zonder logo is dubbelzinnig. Een scan zonder hoofding, als bijlage bij een mail met onderwerp “creditnota 2026-0442” van een gekende leverancier, is dat niet. Een e-mail zonder bijlage wordt als tekstdocument ingelezen, zodat hij nog altijd door classificatie en nazicht loopt in plaats van te verdwijnen.
Er is niets Belgisch aan de code
De classificatie van Konvoi kende Belgische verkeersboetes. De types stonden in de code, de prompts noemden ze bij naam, en een nieuw type toevoegen was een deploy.
In Sluice definieert een workspace haar eigen documenttypes: een naam, aanwijzingen voor classificatie, en een veldschema. De classificatieprompts worden uit dat register gegenereerd, en de extractie draait het model tegen het schema van elk type. Er zit geen enkele ingebouwde taxonomie in.
Dat is het verschil tussen een wagenparkproduct en een platform.
Een schrijnwerkerij definieert leveringsbons en aankoopfacturen. Een praktijk definieert verwijsbrieven en labresultaten. Geen van beide moet op ons wachten.
Daarna landt alles in een nazichtinbox die bewust niets van documenttypes afweet. Het model classificeert en extraheert, een mens bevestigt of corrigeert, en het bevestigde resultaat gaat door naar het systeem dat erom vroeg. We verkopen niet het idee dat het model altijd juist zit. We verkopen het idee dat de correctie acht seconden duurt en op één plek gebeurt.
Waar de parsingdiensten stoppen
Er bestaat een hele categorie goede tools die een gemailed document omzetten in gestructureerde JSON. Wij gebruiken dat patroon zelf. Het stopt op vier plaatsen vóór wat een bedrijf echt nodig heeft.
Breedte van instroom. E-mail en een API zijn niet alle deuren. De printer, de SFTP-drop, de gedeelde mailbox met geschiedenis, de overheidsbus. De moeilijke kanalen zijn net diegene waar een gevestigd bedrijf al van afhangt.
Bewaring. De meeste parsers houden een document bij zolang het uitkomt. Een bedrijf heeft het origineel nodig in 2033, want een betwisting gaat over wat er binnenkwam, niet over wat eruit geëxtraheerd werd.
De transactie. Elke bijlage als losstaand bestand behandelen gooit de enveloppe weg, en die is vaak het sterkste signaal in de hele instroom.
Het laatste stuk. Extractie is halverwege. Aflevering in het systeem dat het verbruikt, met herpogingen, herkomst en een nazichtspoor, is de andere helft, en dat is de helft die bepaalt of iemand er echt mee stopt het manueel te doen.
Wat we er echt uit leerden
Het punt is niet dat we een product gelanceerd hebben. Het punt is waarom er een product te lanceren viel.
De instroomlaag in Konvoi was generiek, en ze was generiek omdat we precies één keer slordig waren geweest met koppeling, dat gemerkt hadden, en het gerepareerd hadden. Alles liep door één naad. De taxonomie was het enige Belgische stuk. Eruit halen was een kwestie van hardgecodeerde types vervangen door een register en het wagenparkjargon te schrappen.
De meeste interne componenten kan je zo niet extraheren, omdat hun algemeenheid nooit echt was. De test is simpel en de moeite om vroeg te stellen: zou dit kunnen draaien voor een klant in een totaal andere sector als je één bestand wist? Zo ja, dan zit er een product verstopt in je project. Zo nee, dan heb je een project.
Sluice is degene die we gevonden hebben.