How Dreamalizing runs a language model without the cloud
A dream is the most personal thing a person can write down. That is why dream text in Dreamalizing never goes to an external AI service. This article describes how that works technically, which decisions followed from it, and what went wrong along the way.
It is written for developers, and for anyone who wants to check whether the privacy promise is more than a sentence on the homepage.
The promise, in one sentence
Your dream text is processed by our own language model on servers within the European Union. Your data is stored encrypted and your dream text is not sent to external AI services.
That sentence appears in the app, in the stores and on this site. Everything below exists to keep it true, even when someone makes a typo in a config file.
The chain, from phone to model
The app (React Native, Expo) talks to an API (NestJS, PostgreSQL) on a rented server in the EU. That server has no language model. Through a WireGuard tunnel it reaches a gateway on our own hardware, and the gateway talks to the model.
The model is Gemma 4 with 26 billion parameters in a mixture-of-experts layout, about 4 billion of them active per token, at 8-bit quantisation. One model does everything: the questions, the analysis and the structured extraction. The context window is pinned at 32,768 tokens, because the heaviest analysis so far used 13,671 tokens and output may run to 8,192.
Until August there were two models: a heavier one for questions and analysis, a lighter one for extraction. That cost more than it saved. The two models evicted each other from memory, so each dream loaded a model three times at 7 to 10 seconds a load. The heavier model does extraction 3.7 seconds slower, but saves 27 seconds of loading.
Local-only as a guard, not an intention
The API has a routing layer that can send requests to several providers. That is convenient in development and dangerous in production. So there are three locks.
First, a hard flag in the runtime. When it is on, the API refuses any call carrying dream content to a non-local provider. An emergency bypass exists, but it is bound to an end date of at most fourteen days and expires on its own; an invalid or too-distant date is ignored and the guard stays shut.
Second, the gateway. Every model id the API requests starts with the prefix local-. The gateway reads that as local-only and refuses to deliver such a request to a cloud provider, even when a fallback chain points there.
Third, tests. One test enforces that the API never strips the prefix; another that the model configuration of staging and production is identical. The first test exists because until late August the API did strip the prefix. In theory the gateway's fallback could then have sent dream text to the cloud, with status 200 and no signal. The app had not launched yet and had no active users besides ourselves. We closed the path and added the test, so that it can no longer happen, silently or otherwise.
Latency is a design decision
A local model on one machine is slower than a cloud API. Questions arrive after 20 to 40 seconds, the analysis after 30 to 60. We do not hide that; the app says what is happening and roughly how long it takes.
The order of operations is built around it. First the questions go to the app, generated from the raw dream text. Then, in parallel, a second call builds the structured data the analysis will need later. Meanwhile the dreamer is answering questions and never notices the second call.
On the machine a limiter allows one call per user at a time and never lets optional calls go ahead of mandatory ones. We learned the latter from a production incident: an optional call crowded out the mandatory one, and the dreamer was waiting on nothing.
Storage and offline
Dream text and answers are stored encrypted per user with AES-256-GCM and a per-user key, which in turn sits under a master key. On the phone, the queue of dreams not yet sent lives in an encrypted SQLCipher database with a key in the device's secure storage.
Every write carries a stable operation id. If the connection drops after sending but before the reply, a retry yields the same dream, not a second one. The ledger of those ids never contains dream text, only the kind of action, a hash and the result.
What it costs
One machine is one ceiling. With concurrent users the calls queue up and latency grows. That is the price of no cloud, and the reason we started in the Netherlands rather than worldwide.
The second price is that the model is weaker than the largest cloud models. So we always measure prompt changes on the local model, never on a cloud model; what works on a bigger model does not automatically work here.
What's next
Questions about the architecture are welcome. What is written here is the state of September 2026; when something changes, this article changes with it.