Skip to content

Auth Module

The Auth module is not part of the API server — it is a standalone service (apps/auth) that the API server proxies to over gRPC. Putting auth in its own process is what lets the platform independently scale the high-RPS validation surface, deploy auth-only patches without touching the API server, and revoke tokens via a Redis-backed bloom filter without hot-reloading the API.

The module’s source lives in pkg/auth/module/ because both the standalone auth server (apps/auth) and integrated-mode API builds reuse the same controller/service/repository code — the difference is just whether it’s mounted as the only thing in the process or as one of several modules.

Class Diagram

uses (HTTP path)

uses (gRPC path)

uses

revocation check

Controller

-service: Service

-logger: Logger

+Register(w, r)

+Login(w, r)

+Refresh(w, r)

+Logout(w, r)

+Me(w, r)

+ForgotPassword(w, r)

+ResetPassword(w, r)

+CheckUsername(w, r)

+CheckEmail(w, r)

Service

-repo: Repository

-bloom: BloomFilter

-mailer: Mailer

-logger: Logger

+Register()

+Login()

+RefreshToken()

+Logout()

+PerformLogoutCleanup()

+GetUserByID()

+ValidateToken()

+CheckUsernameTaken()

+CheckEmailTaken()

Repository

-db: Database

+InsertUser()

+FindByEmail()

+FindByUsername()

+UpdatePasswordHash()

+InsertResetToken()

+ConsumeResetToken()

GrpcHandler

-service: Service

+Login(ctx, req) : Reply

+Register(ctx, req) : Reply

+Refresh(ctx, req) : Reply

+Validate(ctx, req) : Reply

+ValidateAPIKey(ctx, req) : Reply

BloomFilter

Fundamental Purpose

The module owns three things:

  1. Credential lifecycle — registration, login, password reset, account closure. Everything that mints, refreshes, or burns a JWT.
  2. Token validation — every protected request on the API server calls Validate(token) over gRPC. The auth server checks signature, expiry, and the revocation bloom filter, then returns the decoded subject.
  3. API-key validation — the same gRPC handler also validates application-issued X-API-Key headers (delegating to the Applications module’s hashed-key index). The API server doesn’t need to know whether a request is JWT-authed or key-authed; it just gets back a (userID, role, scopes) tuple.

HTTP Surface (auth server)

The auth server exposes its own HTTP surface separately from gRPC. Routes are mounted under <APIPrefix>/auth (default /api/v1/auth).

MethodPathHandlerPurpose
POST/registerRegisterCreate account; returns access + refresh tokens
POST/loginLoginEmail/password → tokens
POST/refreshRefreshRefresh-token → new access (+ rotated refresh)
POST/logoutLogoutRevoke refresh token; add JWT id to bloom filter
GET/meMeDecoded subject from the bearer token
POST/forgot-passwordForgotPasswordIssue a one-time reset token; emails it
POST/reset-passwordResetPasswordConsume reset token; update password hash
GET/check-usernameCheckUsernameAvailability check for the registration form
GET/check-emailCheckEmailAvailability check

gRPC Surface

Defined in contracts/grpc/auth.proto. This is what the API server uses on every protected request.

rpc Login(LoginRequest) returns (LoginReply);
rpc Register(RegisterRequest) returns (RegisterReply);
rpc Refresh(RefreshRequest) returns (RefreshReply);
rpc Validate(ValidateRequest) returns (ValidateReply);
rpc ValidateAPIKey(APIKeyRequest) returns (APIKeyReply);

Validate is the hot path. Everything else is roughly a 1:1 mirror of the HTTP surface (so internal services that prefer gRPC don’t have to talk HTTP).

Special Behaviors

Bloom-filter-backed revocation

When a user logs out, the JWT’s jti claim is added to a Redis-backed bloom filter (one per network). Validate checks the bloom filter on every call. False positives are possible but bounded by the filter’s tuning; they cause a single extra Redis lookup against an exact-match revocation set. False negatives are not possible — once added, a token is permanently flagged within the JWT’s lifetime.

This trades a tiny amount of latency on every Validate for the ability to immediately invalidate any token without scanning the user table. The API server doesn’t have to participate at all — it just consumes the gRPC response.

Refresh token rotation

Every successful Refresh issues a new refresh token and invalidates the old one in the database. A leaked refresh token is therefore single-use; the legitimate user’s next refresh detects the prior-use case and force-logs the session out. This is implemented in Service.RefreshToken (pkg/auth/module/service.go:302).

Shared JWT secret across servers

The auth server signs tokens with AUTH_JWT_SECRET; the API server’s middleware verifies them with the same secret. Both .env files must agree — if they drift, every protected request 401s. The setup script (tools/setup/install.mjs) writes both files in lockstep to prevent this.

Database-shared, write-disjoint with Users module

Both servers share the same Postgres database and the same users table, but they write disjoint columns. The auth server owns password_hash, refresh_tokens, and password_reset_tokens. The Users module owns the profile columns. Reads are unrestricted; writes are coordinated by which service owns the column. There’s no migration story for this — the schema is in apps/api/migrations/ and both services read it.

Logout cleanup hooks

PerformLogoutCleanup(userID) is a separate method (not called on a normal logout) that wipes derived state — refresh-token rows, password-reset rows, etc. It exists for two cases: account deletion (the Users module calls it after marking the user soft-deleted), and admin-triggered force-logout-everywhere flows.

SDK Surface

  • AuthService — login, register, logout, refresh, me, forgotPassword, resetPassword, checkUsername, checkEmail.
  • AuthResponse — generated wire shape for token-issuing endpoints.
  • SessionManager — per-network session storage; persists access + refresh tokens to localStorage and is the single source of truth for “is the user logged in here?”

Auth flows are intentionally not action-ized in the SDK — their inverses are unsafe or undefined (you can’t “un-register” a user or “un-revoke” a token). See the user actions notes in the SDK docs for the rationale.

Cross-Module Integration

ModuleRelationship
UsersShares the users table; auth writes credential columns, Users module writes profile columns.
ApplicationsThe gRPC ValidateAPIKey handler delegates to this module.
API middleware (pkg/middleware)Calls Validate on every protected request, caches the decoded subject for the request lifetime.

Configuration

Env varBoth servers?Notes
AUTH_JWT_SECRET✅ Must matchHMAC key for token signing/verification
AUTH_JWT_EXPIRYAuth onlyDefault 24h
AUTH_JWT_REFRESH_EXPIRYAuth onlyDefault 168h (7 days)
AUTH_BCRYPT_COSTAuth onlyPassword-hash cost factor
MICROSERVICE_AUTH_HOSTAPI onlyWhere the API finds the auth gRPC server

API Reference

See the interactive API reference /auth tag for live examples.