Authentication architecture for startups that want to scale

Four components, clear boundaries, no surprises at 100k users. Boring architecture, predictable performance.

· LoginWith team

The goal of good auth architecture isn’t cleverness. It’s to have zero surprises when you grow from 10 to 10,000 to 100,000 users. Here’s the four-component shape that scales predictably.

Component 1: Identity layer

The identity layer answers “who is this user?” It owns:

  • User records (one per human identity)
  • Membership records (user in tenant with role)
  • Credential records (password hashes, OAuth provider subjects, WebAuthn credentials)

This layer is typically your Postgres database, or a managed identity service like LoginWith. It’s the only place with ground truth about identity.

Boundary: other services don’t read this directly. They read through the identity layer’s API.

Component 2: Session layer

The session layer answers “is this request authenticated?” It owns:

  • Session records (session_id → user_id + metadata)
  • Token records (for API keys, refresh tokens)
  • Session lifecycle (creation, expiration, revocation)

This is usually your main database for up to 100k users, then moves to Redis/Valkey as session-lookup latency becomes relevant.

Boundary: the session layer is consulted on every authenticated request. It should be fast (< 1ms) and always available. If it goes down, your product goes down.

Component 3: Request authentication

The request-auth layer is the middleware that runs on every request. It:

  • Reads the session cookie (or bearer token)
  • Looks up the session in the session layer
  • Attaches the user + tenant context to the request
  • Rejects the request if the session is invalid

This is 50 lines of code that never changes once it’s written. Keep it simple. Don’t put business logic here.

Boundary: downstream handlers receive req.user and req.tenant and can assume they’re valid. If either is null, the handler decides whether that’s OK (public endpoint) or a 401.

Component 4: Tenant context

The tenant context layer scopes all data access to the current tenant. It:

  • Derives the “current tenant” from the session or request (subdomain, header, URL path, session)
  • Enforces that every query is filtered by the current tenant
  • Optionally: uses Postgres row-level security to enforce this at the DB layer

Boundary: every query in your app goes through this. No endpoint bypasses it. No admin UI bypasses it without explicit opt-in.

What not to do

Don’t collapse these layers. Specifically:

  • Don’t put identity in the session. Session records should reference user IDs, not embed user data. When a user changes their email, you shouldn’t have to update 50 session records.
  • Don’t embed tenant logic in request auth. Request auth says “valid user.” Tenant context says “this user’s current tenant is X.” Keep them separable.
  • Don’t let application code read raw session data. Always go through the auth middleware — it’s the only place that validates.

The scaling path

  • 10 users: Postgres, in-process sessions, one database
  • 1k users: same, plus indexes, plus a session cleanup cron
  • 10k users: same, plus maybe a dedicated auth worker pool
  • 100k users: move session layer to Redis, keep identity layer in Postgres
  • 1M users: shard identity by user_id range, keep session layer in Redis cluster

At every step, the component boundaries stay the same. That’s the point.

Want auth that just works?

Get started with LoginWith