Hoe Dreamalizing een taalmodel draait zonder cloud
Een droom is het meest persoonlijke dat iemand kan opschrijven. Daarom gaat de droomtekst in Dreamalizing nooit naar een externe AI-dienst. Dit artikel beschrijft hoe dat technisch in elkaar zit, welke keuzes daaruit volgden en wat er onderweg misging.
Het is geschreven voor ontwikkelaars en voor iedereen die wil controleren of de privacybelofte meer is dan een zin op de homepage.
De belofte, in één zin
Je droomtekst wordt verwerkt door ons eigen taalmodel op servers binnen de Europese Unie. Je gegevens worden versleuteld opgeslagen en je droomtekst wordt niet naar externe AI-diensten gestuurd.
Die zin staat in de app, in de winkels en op deze site. Alles hieronder bestaat om hem waar te maken, ook als iemand een typfout maakt in een configuratiebestand.
De keten, van telefoon tot model
De app (React Native, Expo) praat met een API (NestJS, PostgreSQL) op een gehuurde server in de EU. Die server heeft geen taalmodel. Hij bereikt via een WireGuard-tunnel een gateway op eigen hardware, en die gateway praat met het model.
Het model is Gemma 4 met 26 miljard parameters in een mixture-of-experts-opzet, waarvan ongeveer 4 miljard per token actief zijn, in 8-bits kwantisatie. Eén model doet alles: de vragen, de analyse en de gestructureerde extractie. Het contextvenster staat vast op 32.768 tokens, omdat de zwaarste analyse tot nu toe 13.671 tokens gebruikte en de uitvoer tot 8.192 tokens mag zijn.
Tot augustus draaiden er twee modellen: een zwaarder voor de vragen en de analyse, een lichter voor de extractie. Dat bleek duurder dan het opleverde. De twee modellen verdrongen elkaar in het geheugen, waardoor per droom drie keer een model werd geladen à 7 tot 10 seconden. Het zwaardere model doet de extractie 3,7 seconden trager, maar bespaart 27 seconden laadtijd.
Lokaal-only als bewaking, niet als bedoeling
De API heeft een routeringslaag die naar meerdere aanbieders kán sturen. Dat is handig bij ontwikkeling en gevaarlijk in productie. Daarom zijn er drie sloten.
Ten eerste een harde vlag in de runtime. Staat die aan, dan weigert de API elke aanroep met droominhoud naar een niet-lokale aanbieder. Een noodklep bestaat, maar is aan een einddatum gebonden van maximaal veertien dagen en verloopt vanzelf; een ongeldige of te verre datum wordt genegeerd en de bewaking blijft dicht.
Ten tweede de gateway. Elke model-id die de API opvraagt begint met het voorvoegsel local-. De gateway leest dat als lokaal-only en weigert zo'n verzoek bij een cloudaanbieder af te leveren, ook als een uitwijkketen daarheen wijst.
Ten derde tests. Een test dwingt af dat de API het voorvoegsel nooit strip, een andere dat de modelconfiguratie van test- en productieomgeving identiek zijn. Die eerste test bestaat omdat de API het voorvoegsel tot eind augustus wél stripte. Daardoor had de uitwijk van de gateway in theorie droomtekst naar de cloud kunnen sturen, met status 200 en zonder signaal. De app was toen nog niet gelanceerd en had buiten onszelf geen actieve gebruikers. Wij hebben het pad gesloten en de test toegevoegd, zodat het niet meer kán, ook niet ongemerkt.
Wachttijd is een ontwerpkeuze
Een lokaal model op één machine is trager dan een cloud-API. De vragen zijn er na 20 tot 40 seconden, de analyse na 30 tot 60. Dat verbergen we niet; de app zegt wat er gebeurt en hoe lang het ongeveer duurt.
De volgorde is daarop ingericht. Eerst gaan de vragen naar de app, op de ruwe droomtekst. Daarna, parallel, bouwt een tweede aanroep de gestructureerde gegevens die de analyse later nodig heeft. De dromer beantwoordt intussen vragen en merkt van die tweede aanroep niets.
Op de machine draait een begrenzer die per gebruiker één aanroep tegelijk toelaat en optionele aanroepen nooit vóór de verplichte laat gaan. Dat laatste leerden we uit een productie-incident: een optionele aanroep verdrong de verplichte, en de dromer wachtte op niets.
Opslag en offline
Droomtekst en antwoorden worden per gebruiker versleuteld opgeslagen met AES-256-GCM en een eigen sleutel per gebruiker, die op zijn beurt onder een hoofdsleutel ligt. Op de telefoon staat de wachtrij van nog niet verzonden dromen in een versleutelde SQLCipher-database met een sleutel in de beveiligde opslag van het toestel.
Elke schrijfactie draagt een stabiele operatie-id. Valt de verbinding weg na het verzenden maar vóór het antwoord, dan levert een herhaling dezelfde droom op, niet een tweede. Het grootboek van die id's bevat nooit droomtekst, alleen de soort actie, een hash en het resultaat.
Wat het kost
Eén machine is één plafond. Bij gelijktijdige gebruikers stapelen de aanroepen zich op en groeit de wachttijd. Dat is de prijs van geen cloud, en de reden dat we in Nederland zijn begonnen en niet wereldwijd.
De tweede prijs is dat het model minder goed is dan de grootste cloudmodellen. Wij meten promptwijzigingen daarom altijd op het lokale model, nooit op een cloudmodel; wat op een groter model werkt, werkt hier niet vanzelf.
Wat volgt
Vragen over de architectuur zijn welkom. Wat hier staat, is de toestand van september 2026; als er iets verandert, verandert dit artikel mee.