Authorization Standards
Yew Search uses cookie-based session authentication for all user interactions. This document explains our authentication architecture, the rationale behind choosing cookies over JWT, and how sessions are actually managed in the codebase today.
Why Cookies Over JWT?
Yew Search exclusively uses cookie-based sessions and never uses JWT tokens for authentication. This is a deliberate architectural decision driven by privacy and security requirements.
The Remote Logout Problem
The fundamental issue with JWT tokens is that they cannot be invalidated remotely:
JWT Tokens:
- Self-contained and stateless
- Validated using cryptographic signature alone
- Not stored in any central database or session store
- Valid until they expire (typically hours or days)
- Cannot be revoked or invalidated before expiration
Cookie-Based Sessions:
- Session ID stored in cookie, session data stored server-side (Redis)
- Validated against the session store on every request
- Can be deleted from the session store at any time
- Immediately invalidated when removed from session store
Why Remote Logout Matters
For a privacy-focused search engine handling sensitive personal data, remote logout is critical: if we detect suspicious activity or a user reports their account compromised, we can terminate sessions instantly, without waiting for a token to expire.
Real Architecture
Unlike a typical /auth module, Yew Search has no dedicated authentication controller or /v1/auth/* namespace at all. Login, logout, and session management are all just CRUD operations on the UserSession resource.
Dual storage: Redis for the live session, Postgres for the audit trail
- Redis (via
express-session+connect-redis, wired insrc/main.ts) is the actual authentication store. The session cookie (connect.sidby default) carries a session ID thatexpress-sessionresolves against Redis on every request - this is the fast path every authenticated request goes through. - Postgres (
user_sessiontable,UserSessionEntity) is a separate, parallel record of sessions - it's what backs the session-management UI (showing a user every active session, letting them terminate one). It is not consulted on every request; only the session-management endpoints read/write it directly.
UserSessionEntity (src/v1/user-session/entities/user-session.entity.ts)
@Entity('user_session')
export class UserSessionEntity {
id: string; // UUID primary key
sessionId: string; // the express-session id - unique
userId: string; // uuid, the owning user
ipAddress: string | null;
userAgent: string | null;
expiresAt: Date;
createdAt: Date;
updatedAt: Date;
}
There is no lastAccessedAt or metadata field - this row isn't touched on every request, only on login (and re-login, see below).
Login: POST /v1/user-session
UserSessionController.createOne() (src/v1/user-session/user-session.controller.ts):
- Looks up the user by email, verifies the password with Argon2 (
UserEntity.verifyPassword). UserSessionService.createOne()either inserts a newuser_sessionrow keyed by the currentreq.session.id, or - if a row for thatsessionIdalready exists (the same browser session logging in again) - updates it in place instead of inserting a duplicate, sincesessionIdis a unique column.- Sets
req.session.userIdon the real Redis-backed session - this is the actual authentication state.
There is no registration endpoint. Users are created exclusively via the CLI (user:create, see List of CLI Commands).
Request authentication: AuthorizationGuard
src/common/auth/authorization.guard.ts - much simpler than a typical session guard:
@Injectable()
export class AuthorizationGuard implements CanActivate {
constructor(private readonly userService: UserService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
if (!request.session || !request.session.userId) {
return false;
}
const user = await this.userService.readOne({
user: 'system',
id: request.session.userId,
});
request.user = user;
return true;
}
}
It checks the Redis-backed request.session.userId (set at login), loads the user, and attaches it to the request. That's the entire guard - no token parsing, no database session lookup on this path.
Logout: DELETE /v1/user-session/me
Deletes the current user_session Postgres row and destroys the real Redis-backed session (req.session.destroy()), which is what actually logs the user out.
Session management endpoints
All on UserSessionController, all guarded by AuthorizationGuard:
GET /v1/user-session- list the current user's sessions (reads the Postgres table, for display)GET /v1/user-session/me- the current sessionGET /v1/user-session/:id- a specific session, scoped to the callerDELETE /v1/user-session/:id- terminate a specific session's Postgres row
There is no "logout all devices" or admin force-logout endpoint today.
Cookie Configuration
The real express-session cookie config (src/main.ts):
cookie: {
httpOnly: true,
secure: false, // Traefik terminates TLS and forwards plain HTTP, so this must stay false
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
domain: NODE_ENV === 'production' ? (SESSION_DOMAIN || '.yewsearch.com') : undefined,
}
Note this is not secure: true / sameSite: 'strict' - secure: true would break cookies entirely behind Traefik's HTTP-to-the-backend forwarding, and sameSite: 'lax' (not 'strict') is what actually ships.
Password Storage
Passwords are hashed using Argon2 (UserEntity.verifyPassword), never bcrypt, MD5/SHA, or plain text.
What Doesn't Exist Yet
Worth being explicit about, since it's easy to assume a system this size already has these: there is no rate limiting (@Throttle) on the login endpoint, no CSRF token mechanism beyond the sameSite cookie attribute, no session-fixation regeneration on login, and no MFA. None of this is a secret gap - it's just not built. If you're adding any of it, this is the file to update once it lands.
Trade-offs and Limitations
Cookies + Redis (the live path):
- Scales horizontally (stateless application servers, shared Redis)
- Fast session lookups (in-memory)
- Redis failure means all live sessions are lost
Cookies + Postgres (the audit/management path):
- Lets users see and manage their own active sessions
- Not consulted on the hot authentication path, so its latency doesn't matter the same way
JWT Advantages We're Giving Up
JWT needs no session store and no per-request lookup, and verifies independently across services - none of which matters here, since Yew Search is a monolith and remote logout matters more than a marginal performance gain.
Summary
Yew Search uses cookie-based sessions exclusively because:
- Remote logout is critical - sessions can be terminated immediately when needed
- Privacy and security first - users trust us with sensitive data
- JWT's benefits don't apply - we're a monolith, not microservices
The actual implementation is intentionally thin: a UserSession CRUD resource, one guard, and Redis doing the real authentication work underneath express-session.