Skip to content

How apps remember you

You log in. You click something. The server has no idea who you are.

HTTP is stateless: every request arrives fresh, with no memory of the one before it and no built-in way to connect the two. The login that succeeded a second ago left nothing behind.

So an app that needs to recognise you across requests has to put identity somewhere and get it back on every request that follows.

The whole of this section is one question: where does identity live?

Almost every login flow on the web answers it in one of three ways.

The server remembers

In stateful identity, the server keeps the information.

When you log in, the server creates a session, a small bundle of data representing you: a user id, a name, maybe a role. That session lives on the server. The browser gets a cookie carrying a session id, nothing more, and sends it automatically with every request to that domain. The server takes the id, looks up the session, and knows who you are again.

This is the traditional web app pattern, and the default in a lot of frameworks. If you've used express-session, this is what it was doing.

JunoThe server remembers The bit worth holding onto is how little the browser is trusted with. It gets an id and nothing else.

Everything that matters about you stays on the server, and the id is only useful to someone who can also reach that server.

JunoThe server remembers The lookup on every request is the thing to notice, because it's both the strength and the cost. It means the server can change or destroy a session at any moment and the next request feels it immediately.

It also means every request touches shared storage. On one server that's memory and free. Across several it's a store they all reach, which is the first piece of infrastructure this model asks you to run.

JunoThe server remembers Instant revocation is the property you're buying, and it's worth more than it sounds. Fire someone, flip a role, respond to a stolen laptop: delete the session and the next request is anonymous. No waiting for anything to expire.

What you pay is a coordination problem that grows with your deployment. Sessions in process memory die with the process and do not exist for the instance next to it, so any horizontal scaling needs a shared store, and that store becomes something that has to stay up for anyone to stay logged in.

Sticky sessions are the tempting shortcut and they trade the problem for a worse one: your load balancing is now tied to your auth, and losing one instance logs out everybody it was serving.

The client carries the proof

In stateless identity, the server stores nothing about you.

The client holds a token and sends it with every request. The server checks the token and answers, without looking anything up about who you are between requests.

That works well for APIs, mobile apps and distributed systems, where shared server memory is awkward or unavailable. If you've ever called an API and received 401 Unauthorized: missing authorization header, that's this model telling you to bring your proof.

There are two versions of it, and the difference matters enough that they get a chapter each. An opaque bearer token is a meaningless random string the server looks up. A JSON Web Token carries the identity inside itself, signed, so no lookup is needed at all.

JunoThe client carries the proof The name is a little misleading at first. The server is stateless about you, not about everything.

It still has a database full of users. What it stops keeping is a record of the fact that you are currently logged in.

JunoThe client carries the proof "Bearer" is the word doing the work in bearer token, and it means what it says: whoever bears it, holds it. The token is not tied to a device, a browser or a network, so a copied token is a working token.

That's why transport and storage matter so much here. HTTPS everywhere, and a hard think about where the client keeps it, because anything readable by JavaScript is readable by injected JavaScript.

JunoThe client carries the proof The tradeoff is exactly inverted from sessions. You gain a server that needs no shared state and scales sideways for free, and you give up the ability to change your mind.

A token you signed is valid until it expires, wherever it is, whatever has happened since. There is no list to delete it from. That's why the usual production shape is a short-lived access token plus a longer-lived refresh token that can be revoked: it reintroduces a lookup, but only on refresh instead of on every request.

Which is worth naming plainly. Fully stateless identity gives up revocation, and anything claiming both has put some state back somewhere.

Somebody else vouches for you

In delegated identity, a third party you trust confirms who the user is on your behalf.

Your app asks Google whether it recognises this person. Google runs the login and hands your app a token proving the answer. Every "Sign in with Google", "Sign in with GitHub" and "Sign in with Apple" button is this.

The appeal is that you stop running a password system. No password storage, no reset flow, no breach of credentials you never held. OAuth and delegated identity covers how the handover actually works.

JunoSomebody else vouches for you Think of it as being vouched for. You don't prove yourself to the app directly. Someone the app already trusts confirms you, and the app takes their word.

That's why you get bounced to a Google page and then back again. The proving happened over there.

JunoSomebody else vouches for you It removes a category of risk and adds a dependency. Your login now goes down when theirs does, and users without an account there cannot sign up at all.

Which is why most consumer apps offer it alongside email and password instead of replacing it, and then have to handle the same person arriving through both doors.

JunoSomebody else vouches for you Worth being precise, because the vocabulary is routinely wrong. OAuth grants delegated authorization: permission for your app to act on a resource. OpenID Connect is the identity layer built on top of it, and the "sign in with" buttons are OIDC. Using an OAuth access token as proof of who someone is, rather than an OIDC identity token, is a real and common mistake.

What you inherit is the provider's decisions. Their session lifetime, their account recovery, their view of what an email address means. If they let someone recover an account by phone number, that's now your account recovery too.

Account linking is the sharp edge. Matching a delegated login to an existing account by email address is the first thing anyone reaches for, and it's only safe when the provider verifies email, which not all of them do.

Side by side

StatefulStatelessDelegated
Where identity livesOn the serverIn the token the client holdsWith the provider
Cost per requestA lookupA signature check, or a lookupDepends on the token you end up using
Revoking accessImmediateHard, usually waits for expiryPartly the provider's decision
Scales sidewaysNeeds a shared storeFreelyFreely
Main thing you give upCoordination between serversThe ability to change your mindControl and independence

None of them is the right answer. They optimise for different things, which is why real systems often run more than one: a delegated login to establish who you are, a session for the website, tokens for the API.

JunoSide by side Don't try to memorise this table yet. It'll mean much more once you've seen each one working.

The one line worth carrying into the next chapter: stateful is for control, stateless is for scale, delegated is for handing the problem to someone else.

JunoSide by side When you meet an unfamiliar system, the fastest way in is to ask where identity is stored. Everything else follows from the answer.

If the server holds it, look for how sessions expire and where they're stored. If the client holds it, look at what happens when a token leaks and how anyone revokes it. If a provider holds it, look at how accounts get linked.

JunoSide by side The revocation row is the one that decides most real arguments, and it's usually discovered late. Teams pick stateless for its scaling properties, ship, and then meet the first incident that requires logging one specific person out immediately.

The honest framing is that you are choosing what to be bad at. Stateful is bad at scaling and good at control. Stateless is the reverse. Anything advertising both has quietly reintroduced a lookup somewhere, which is a perfectly good design, and worth recognising for what it is instead of treating it as a free win.

Try it

For each system, name the model you'd expect and the one property that decides it.

  1. An internal admin dashboard where revoking someone's access has to take effect immediately.
  2. A public API serving a mobile app with a few hundred thousand users.
  3. A photo app whose entire signup is a "Continue with Google" button.
  4. A microservice architecture where a dozen services each need to know who is calling.
Compare your answers
#ModelThe deciding property
1StatefulImmediate revocation. Delete the session and the next request is anonymous. Nothing else offers that.
2StatelessNo shared session store to coordinate as traffic grows, and mobile clients hold a token comfortably.
3DelegatedNo password system to build, store or breach, and the fastest possible signup.
4Stateless, specifically JWTsEach service can verify the token itself without asking a central server on every call.

Number 1 is the interesting one. It's the smallest system on the list and the one with the least scaling pressure, and it still gets the model that scales worst, because control matters more here than growth does.

Where this goes next

Three families, one problem each solves differently.

Sessions and cookies takes the first one apart: what the server stores, what the browser carries, and the mistakes that turn a solid model into a leaky one.