hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Authentication with @hectoday/http

What Is Authentication?

  • Who Are You?
  • HTTP Is Stateless
  • Project Setup

Passwords

  • Why Not Store Passwords Directly
  • Hashing with bcrypt
  • Building a Signup Route
  • Building a Login Route

Sessions and Cookies

  • What Is a Cookie?
  • What Is a Session?
  • Building Session Management
  • Protecting Routes
  • Logout
  • Cookie Security

Tokens

  • What Is a Token?
  • Anatomy of a JWT
  • Creating JWTs
  • Verifying JWTs
  • Sessions vs. Tokens

Putting It Together

  • Authorization
  • Common Mistakes
  • Capstone: User Management API

Sessions vs. tokens

You just built two different approaches to the same problem. Sessions, where the server remembers who you are and the client just holds a reference. Tokens, where the client carries the proof of identity itself and the server just verifies. Both work. Both are real. Both are used by huge production systems in 2026. So which one should you pick for your own projects? This lesson is the honest answer, and that answer is “it depends,” but in a useful way. By the end, you will have a concrete mental model for matching the right tool to the right situation.

How they work, side by side

Let me put the two flows next to each other so the comparison is unavoidable.

Sessions: the server remembers

Login:
  Client → Server: "Here's my email and password"
  Server: verifies credentials
  Server: creates a record in the session store
  Server → Client: Set-Cookie: session=abc123

Subsequent request:
  Client → Server: Cookie: session=abc123 (browser sends automatically)
  Server: looks up "abc123" in the session store → finds user data

The identity lives on the server. The client holds a meaningless reference. The server does a lookup on every request.

Tokens: the client carries the proof

Login:
  Client → Server: "Here's my email and password"
  Server: verifies credentials
  Server: creates a signed token containing user data
  Server → Client: { "token": "eyJhbG..." }
  Server: stores nothing

Subsequent request:
  Client → Server: Authorization: Bearer eyJhbG... (client sets explicitly)
  Server: verifies the signature → reads user data from the token

The identity lives on the client. The server stores nothing. It verifies the token’s signature on every request instead of doing a lookup.

The code you write looks almost identical

This is one of the nicer surprises. In our Hectoday HTTP handlers, both approaches end up looking basically the same:

Session-based:

const caller = authenticate(c.request);
if (caller instanceof Response) return caller;

Token-based:

const caller = await authenticateToken(c.request);
if (caller instanceof Response) return caller;

The only visible difference is the await on the second one (because cryptographic verification is async). After those two lines, the rest of your handler does not care which flavor of auth got used. Both functions hand you back a user (or a Response) and you proceed from there.

That is worth internalizing. The auth mechanism is different. The auth interface is the same.

Comparison

AspectSessionsTokens (JWT)
Where identity livesServer (session store)Client (inside the token)
What the client holdsRandom session ID (cookie)Signed document with user data (header)
Server storageRequired (Map, Redis, database)None
RevocationInstant (delete the session record)Difficult (token is valid until it expires)
Scaling across serversAll servers need access to the session storeAny server with the secret can verify
Transport mechanismCookie (automatic)Authorization header (manual)
Works cross-domainDifficult (cookies are domain-bound)Easy (headers work anywhere)

Let’s walk through when each one shines.

When to use sessions

Your app is a traditional web application. The browser is the only client. Everything lives on the same domain. Cookies get sent automatically, so you do not have to write a single line of client-side code to handle authentication. That is a huge simplification.

You need instant revocation. If a user changes their password, gets banned, or reports a compromised account, you can delete their session immediately. The next request with that session ID fails. With tokens, you cannot do this without extra infrastructure.

You value simplicity. Session-based auth is conceptually simpler. Store a record. Give the client an ID. Look it up later. There is no cryptography to reason about. There are no expiry windows to tune. There are no refresh token flows to build. It just works.

If you are building a SaaS product with a web UI, a SPA talking to a backend on the same domain, or basically any “login to my website” flow, sessions are usually the right call.

When to use tokens

You have non-browser clients. Mobile apps, CLI tools, third-party API consumers, and service-to-service calls do not have built-in cookie handling. Sending a token in an Authorization header is trivial from any HTTP client.

Your API is consumed across domains. If your frontend lives on app.example.com and your API lives on api.example.com, getting cookies to work requires specific CORS configuration and can be brittle. Tokens in headers just work, regardless of domain.

You need to scale horizontally without shared state. With sessions, every server instance needs to reach the same session store. With 20 servers behind a load balancer, they all need to connect to the same Redis. With tokens, any instance that has the signing secret can verify a token independently. No shared state. No lookup round trip. This matters at scale.

You need to pass identity between services. In a microservices architecture, a token can travel from service to service in the Authorization header. Each service verifies it on its own. Passing a session ID between microservices would require every service to talk to the same session store, which is tight coupling you probably do not want.

The revocation problem, in detail

This is the single biggest practical difference between the two, so let’s make it really concrete. Imagine a user emails you saying their account has been compromised. You need to lock them out right now.

With sessions: You delete their session records from the store. Their next request returns 401. Done. One operation. Instant.

With tokens: The token is valid until its exp claim says it is not. If it expires 24 hours from now, the attacker has up to 24 hours of access. Your options for doing better are all compromises:

Token deny list. Keep a list of revoked token IDs on the server. Check every request against it. This works, but now you have server-side state, which was supposed to be the whole point of not using sessions.

Short expiry plus refresh tokens. Set access tokens to expire in 15 minutes. Issue a separate, long-lived “refresh token” that can request new access tokens. Revoke the refresh token to stop new access tokens from being issued. The attacker still has up to 15 minutes of access, but no more. This is the standard pattern for production token systems, but it is genuinely complex to implement correctly.

Change the signing secret. Invalidates every token for every user all at once. It is a sledgehammer, not a scalpel. Useful in a real emergency, not as a routine tool.

None of these are as clean as deleting a session record. This is the real cost of stateless auth, and it is worth knowing about before you adopt it.

Common combinations in real apps

In practice, a lot of production apps use both. Here are two patterns you will see in the wild:

Sessions for web, tokens for API. The browser-based frontend uses session cookies. The mobile app and third-party integrations use tokens. The same backend supports both. This is exactly what you built in this course: /login sets a session cookie, /token/login returns a JWT. Both authenticate functions return the same user shape so handlers do not care which flow happened.

Tokens with a session-like store. You use JWTs for the format and header transport (convenient, stateless-feeling) but you also keep a token ID list on the server so you can revoke them. You get the cross-domain and stateless-ish benefits of tokens with the revocation capability of sessions. Trade-offs go both ways.

There is no universal winner

The answer to “should I use sessions or tokens?” is always “it depends on your architecture.” For a single-domain web app with one backend, sessions are simpler and more secure. For a distributed API serving multiple client types, tokens are often more practical. For most production companies, some mix of both.

The good news is that you have now built both, so you are no longer guessing. You know what is involved in each. When you encounter a project where someone is loudly insisting you must use JWTs, you can ask the real questions: do we actually need cross-domain? Are there non-browser clients? How are we going to handle revocation? And if someone insists on sessions for something that obviously wants tokens (like a public API), you can push back too.

In the next section, we start working on what you do after someone is authenticated. Authentication only answers “who are you?” Now we need to answer “what are you allowed to do?” That is authorization, and it is where the concept of roles finally becomes useful.

A startup is building an API that will be consumed by a web app on the same domain and a mobile app. Which approach fits best?

Why is token revocation harder than session revocation?

← Verifying JWTs Authorization →

© 2026 hectoday. All rights reserved.