acme/web · architecture note

How authentication flows through the codebase

Acme uses cookie-based sessions: the browser never holds a bearer token directly. Every authenticated request hits /api/*, passes through a single verifyToken() middleware, and resolves to a Session row that downstream handlers read off req.ctx. The middleware is the only place that talks to the session store, which is the only place that talks to the sessions table — so there's exactly one trust boundary to reason about.

Request path

Browser acme.app /api/session route handler verifyToken() middleware/auth.ts SessionStore lib/sessionStore.ts Postgres sessions table cookie lookup

Callstack walkthrough

1
src/app/providers/AuthProvider.tsx :22-48

On mount, the React provider issues a GET /api/session with credentials: 'include' so the fw_sid cookie rides along. The response either hydrates currentUser into context or leaves it null, which the router treats as "show the sign-in screen".

show source
// src/app/providers/AuthProvider.tsx
export function AuthProvider({ children }: Props) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    fetch('/api/session', { credentials: 'include' })
      .then(r => r.ok ? r.json() : null)
      .then(setUser);
  }, []);

  return <AuthCtx.Provider value={{ user }}>{children}</AuthCtx.Provider>;
}
2
src/server/routes/session.ts :9-27

The route itself is thin: it just returns whatever req.ctx.session the middleware attached. If the middleware short-circuited with a 401, this handler never runs — so there's no auth logic duplicated here.

show source
// src/server/routes/session.ts
router.get('/session', verifyToken, (req, res) => {
  const { session } = req.ctx;
  res.json({
    id:    session.userId,
    email: session.email,
    role:  session.role,
    exp:   session.expiresAt,
  });
});
3
src/middleware/auth.ts :14-31

This is the trust boundary. verifyToken reads the signed fw_sid cookie, asks SessionStore to resolve it, and either populates req.ctx.session or responds 401. Every protected route in the app is mounted behind this function, so changing its behaviour changes auth globally.

show source
// src/middleware/auth.ts
export async function verifyToken(req, res, next) {
  const raw = req.signedCookies['fw_sid'];
  if (!raw) return res.status(401).end();

  const session = await SessionStore.get(raw);
  if (!session || session.expiresAt < Date.now()) {
    return res.status(401).end();
  }

  req.ctx = { session };
  next();
}
4
src/lib/sessionStore.ts :8-52

SessionStore is a small read-through cache: it checks an in-process LRU first, then falls back to Postgres. Writes (create, revoke) always go straight to the DB and invalidate the cache entry so other workers don't serve a stale session.

show source
// src/lib/sessionStore.ts
const cache = new LRU<string, Session>({ max: 5000, ttl: 60_000 });

export const SessionStore = {
  async get(id: string) {
    const hit = cache.get(id);
    if (hit) return hit;
    const row = await db.one(SELECT_SESSION, [id]);
    if (row) cache.set(id, row);
    return row ?? null;
  },
  /* create, revoke, touch ... */
};
5
db/migrations/004_sessions.sql :1-18

The sessions table is keyed on a random 32-byte id (the cookie value) with a covering index on user_id for "sign out everywhere". Expiry is enforced both here (expires_at) and again in the middleware as defence in depth.

show source
-- db/migrations/004_sessions.sql
create table sessions (
  id          text primary key,
  user_id     uuid not null references users(id),
  created_at  timestamptz default now(),
  expires_at  timestamptz not null,
  ip          inet,
  user_agent  text
);
create index sessions_user_id_idx on sessions(user_id);