The server that remembers nothing

A few weeks ago I was building the guided tour that runs when you first land on this site. The bot walks you through the terminal, then hands you across to /log and keeps going, one continuous sequence. Simple enough, except for one line in the spec that made it hard. The server has to know which step you are on, and the server is not allowed to remember anything about you.

That second half is a real constraint here, not a preference I could bend. The privacy architecture on this project says the server keeps no visitor store. No per-user row, no session record, no IP on disk. A visitor is a browser that shows up, gets served, and leaves no trace on my side. I had already written that down as an invariant and shipped a whole disclosure system around it. So when the tour needed an authoritative step counter, I could not just park the visitor's progress in a server session the way most stateful flows do. The obvious tool was off the table by my own rule.

You can feel the pull toward two easy answers, and both are wrong for this. The first is to let the browser decide the step entirely: it tracks where you are and tells the server "I am on step seven, give me step seven's script." That works right up until the browser and the server disagree about what step seven even means, and now you have two copies of the rules drifting apart every time you touch either one. The second answer is to break the privacy rule just a little and keep a tiny server-side counter. That is how every privacy invariant dies, one reasonable exception at a time.

let the client carry, let the server decide

The move that worked was to split two things people usually fuse. There is the raw state, and there is the interpretation of the raw state, and they do not have to live in the same place. The client carries the raw state: a plain message counter in sessionStorage, a single number that goes up. The server owns the interpretation: a pure function, deriveTourPhase, that takes that number and returns the authoritative step. The same function runs on both sides. The browser runs it to render, the server runs it to decide what to say, and because it is one function with no hidden state, they cannot disagree. The server stays fully authoritative over what step seven means. It just never has to store that you are on it.

The part I liked most is what happens at the page boundary. The terminal lives on the landing page; /log is a hard navigation away, a real page load, the kind that wipes most in-memory state. The counter survives it because sessionStorage is per origin, so the number the terminal was counting is the same number /log picks up. The tour does not restart, does not lose its place, does not ask the server "where were we." The terminal owns the early beats, /log owns the later ones, and the handoff is seamless because the only thing crossing the boundary is one honest number plus a function both pages already agree on. Continuity across the handoff, with nothing handed off but a count.

Underneath, this is the same idea I keep coming back to, the gap between what a system is told and what it works out for itself. I have written about surface versus semantic when a gate reported a pass it never actually ran. Here it points at trust. The counter the client sends is the surface: a claim, easy to read, easy to fake. The step the server derives is the semantic layer: the real answer, computed from rules the server holds. The server reads the claim and then decides for itself. It does not take the browser's word for what step you are on. It takes the browser's number and does the math. Surface proposes, semantic disposes.

It is worth being clear about where this pattern stops, because the honesty is what makes it usable. The server is authoritative over the rules, not over the raw input. A visitor could open devtools, set the counter to a hundred, and skip to the end of the tour. For a tour, that is fine; skipping ahead in a thing you could close anyway costs nothing. The moment the raw input has to be tamper-proof, money, access, anything a user gains by lying, you sign the number or you verify it server-side, and the calculus changes. The shared-function trick buys you drift-free interpretation and zero server storage. It does not buy you trust in the input. Knowing which of those you actually need is the whole decision.

Strip the tour out and the shape is reusable. Any time you want a server to stay authoritative over some flow but you do not want to keep per-user state on the server, whether for privacy, for cost, or just to avoid a session store you will have to operate, you can push the raw state to the client and pull the interpretation into a function both sides share. The vibe coding workflow tends to leave you with client-only state, because that is what comes out of the box, and the usual advice is to go build a backend to make it real. Sometimes you do not need the backend. You need one function that the client and the server both run, and the discipline to keep the rules in that one place.

If you are a vibe coder shipping a vibe coded app with state that has to hold across pages or surfaces, and you are hitting the wall where client-only state feels fragile but a full backend feels like too much, that is a real design fork, and it is worth thinking through before you pour the concrete. VibeKoded can help you scope it, shape the shared-function boundary, or build the thing with this kind of discipline baked in from the start. Work with VibeKoded.

The server remembers nothing about you. It still knows exactly where you are. Those two facts only sound like a contradiction until you decide which side owns the state and which side owns the rules.