Skip to main content
One idea at a time

A session remembers a successful login

After Maya logs in, she should not send her password with every task request. The server creates a session and gives her browser an opaque random token in a cookie.

Opaque means that the token does not encode a user ID or role for the browser to inspect. The server looks it up to discover the authenticated user.

Taskboard stores a hash of the token in PostgreSQL. The raw token goes to the browser, and the hash remains in the database. A read-only database leak therefore does not directly provide the cookie value needed to impersonate Maya.

Every request checks current session state​

The browser includes the taskboard_session cookie in eligible requests. The API hashes that value, finds the session, and verifies its expiry. It then loads the identity represented by the session.

The actual authentication function returns a small Actor value. Its user and session identifiers connect later operations to the authenticated session. The expiry condition belongs in the database lookup.

From the working appRead api/src/auth.ts
export async function authenticate(
database: Database,
sessionToken: string | undefined,
): Promise<Actor> {
if (!sessionToken || sessionToken.length > 100)
throw new AppError(401, 'unauthenticated', 'Sign in to continue.');
const [session] = await database.db
.select()
.from(sessions)
.where(
and(
eq(sessions.tokenHash, hashToken(sessionToken)),
gt(sessions.expiresAt, new Date()),
),
);
if (!session)
throw new AppError(
401,
'unauthenticated',
'Session expired. Sign in again.',
);
return {
userId: session.userId,
sessionId: session.id,
csrfToken: session.csrfToken,
Excerpt shown. The source browser includes the complete function.

Deleting the session revokes it. That makes logout a server-side operation rather than merely removing a value from frontend state. A stolen cookie remains usable if the browser deletes its copy but the server leaves the session valid.

Taskboard uses database-backed sessions because the API and worker already depend on PostgreSQL. Multiple API processes can recognize the same session without sharing process memory.

HttpOnly prevents normal frontend JavaScript from reading the cookie. Secure restricts delivery to HTTPS in production. SameSite limits some cross-site cookie delivery, and the cookie path controls which requests receive it.

These settings reduce exposure, but they do not make an injected script harmless. A script running within the application origin may still make authenticated requests. Preventing cross-site scripting remains necessary.

Session expiry is a server rule. A cookie's browser expiry controls when the browser stops storing it. The database expiry controls whether the server accepts it. The API must check the latter even when a manually crafted client supplies an old cookie.

The session identifies Maya globally. Her workspace permissions come from current membership checks. Storing a workspace role in the browser's state is useful for presentation, but it cannot be the authorization decision.

Why not keep sessions in a JavaScript map?

A restart loses the map, and another API process has a different map. PostgreSQL gives the application shared, persistent session state with explicit revocation.

A session is a revocable server record. The cookie is the credential used to find that record.

Read api/src/auth.ts Read api/src/security.ts