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

What is a session?

We now know what a cookie is: a piece of text the server hands to the browser, which the browser sends back automatically on every request. Perfect. We have the delivery mechanism. What we do not have yet is the thing to deliver. Just putting session=abc123 in a cookie is not enough on its own. We need to design what that string actually means and where the real user data lives. That is what a session is. Let’s build up the idea carefully, because the design decisions here are genuinely clever.

The idea

A session is how we remember who somebody is across multiple requests. Here is how it works, step by step:

  1. The user logs in.
  2. The server creates a record that says “session xyz789 belongs to User 42.”
  3. The server sends that session ID (xyz789) to the browser as a cookie.
  4. On every future request, the browser sends the cookie back.
  5. The server reads the session ID from the cookie, looks it up in its record, and now knows this is User 42.

The session ID itself is just a random string. It has no meaning on its own. It is a key into a lookup table on the server. All the real data (who the user is, when they logged in, what role they have) lives on the server. The cookie just holds the key.

What lives where

This is the key mental model, and if you get only one thing from this lesson, get this:

On the client (inside the cookie): Just the session ID. A random string like "f47ac10b-58cc-4372-a567-0e02b2c3d479". Nothing sensitive.

On the server (in the session store): The actual session data. User ID, email, role, when it was created, whatever you need.

Here is a picture:

Client cookie:
  session=f47ac10b-58cc-4372-a567-0e02b2c3d479

Server session store:
  "f47ac10b-58cc-4372-a567-0e02b2c3d479" → {
    userId: "42",
    email: "[email protected]",
    role: "user",
    createdAt: 1700000000000
  }

The client never sees the session data. The client only holds the key. That has some really nice consequences:

  • The client cannot tamper with the session data, because it does not have it.
  • The server has total control over what is in the session and can update or delete it any time.
  • Logging out means the server deletes its record. The cookie is now a key to nothing, so even if the attacker still has it, it unlocks nothing.

This is like a coat-check ticket. Your ticket is just a number. All the useful information (which coat is yours, when you dropped it off) lives with the coat-check attendant. If you lose the ticket, you can’t claim the coat. If the attendant throws away the ticket, they still have the coat but now nobody can claim it. That is exactly how sessions work.

Why not just stuff everything in the cookie?

OK, reasonable question: why go through all this server-side storage stuff? Why not just put the user data right in the cookie?

Set-Cookie: user={"id":"42","email":"[email protected]","role":"user"}

This is called a client-side session. The data travels with the request, no server-side storage needed. It sounds simpler. Why don’t we do that?

Three big problems.

Problem 1: Tampering

The client can modify cookie values. Nothing stops them. A user could open their browser devtools, find the cookie, and change "role":"user" to "role":"admin". Without a cryptographic signature, the server has absolutely no way to detect the change. It would just see "admin" and grant admin access. Game over.

Problem 2: Size

Cookies have a size limit (typically 4 KB per cookie). Session data can outgrow that fast, especially if you start adding more fields to it over time.

Problem 3: Revocation

This one is subtle but huge. Imagine a user reports that their account has been compromised. You want to lock them out right now. With server-side sessions, you just delete the session record. Their next request has a cookie pointing to nothing, and they are out. Done. Instant.

With client-side sessions, you have no server-side record to delete. The data lives on the client. Even if you tell the browser to clear the cookie, a motivated attacker can just resend the old cookie value from their own tools. You cannot take it back. It is already out in the world.

Server-side sessions dodge all three problems at once. The cookie is a meaningless random ID. The server is the single source of truth for anything that actually matters.

[!NOTE] JWTs (which we cover in Section 4) are essentially signed client-side sessions. The cryptographic signature solves the tampering problem, but they still inherit the revocation problem. We will get into this comparison in detail later.

A simple session store

At the code level, a session store is just a lookup table. The simplest possible version is a Map:

interface Session {
  userId: string;
  email: string;
  role: "user" | "admin";
  createdAt: number;
}

const sessions = new Map<string, Session>();

To create a session, you generate a random ID, put the data in the map:

const sessionId = crypto.randomUUID();
sessions.set(sessionId, {
  userId: user.id,
  email: user.email,
  role: user.role,
  createdAt: Date.now(),
});

To look up a session:

const session = sessions.get(sessionId);
// session is Session | undefined

To delete a session (this is what logout does):

sessions.delete(sessionId);

That is the whole interface. Create, read, delete. In production you would use Redis or a database instead of an in-memory Map, but the shape of the API is identical. You are still just storing data under a random key and looking it up later.

The flow, all at once

Let’s trace the whole thing end to end so you can see it as one coherent story:

1. POST /login { email, password }
   → Server verifies credentials
   → Server creates session: sessions.set("xyz789", { userId: "42", ... })
   → Server responds with: Set-Cookie: session=xyz789

2. GET /dashboard
   → Browser sends: Cookie: session=xyz789 (automatically)
   → Server reads cookie, calls sessions.get("xyz789")
   → Server gets { userId: "42", ... }
   → Server knows this is User 42, returns their dashboard

3. POST /logout
   → Browser sends: Cookie: session=xyz789
   → Server calls sessions.delete("xyz789")
   → Server responds with: Set-Cookie: session=; Max-Age=0
   → Browser deletes the cookie

4. GET /dashboard (after logout)
   → Browser sends no cookie (it was deleted)
   → Server has no session ID to look up
   → Server returns 401 Unauthorized

Step by step, nothing fancy happens. A random ID flies back and forth in a cookie. The real data sits on the server. This is, conceptually, exactly what session-based auth is, even in the biggest production applications.

In the next lesson, we are going to build this for real in code. Session store, helper functions, and we will finally wire it into our login route so the server actually remembers who logged in.

Why is a random session ID better than putting user data directly in the cookie?

← What Is a Cookie? Building Session Management →

© 2026 hectoday. All rights reserved.