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
Fundamental Purpose
The module owns three things:
- Credential lifecycle — registration, login, password reset, account closure. Everything that mints, refreshes, or burns a JWT.
- 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. - API-key validation — the same gRPC handler also validates application-issued
X-API-Keyheaders (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).
| Method | Path | Handler | Purpose |
|---|---|---|---|
POST | /register | Register | Create account; returns access + refresh tokens |
POST | /login | Login | Email/password → tokens |
POST | /refresh | Refresh | Refresh-token → new access (+ rotated refresh) |
POST | /logout | Logout | Revoke refresh token; add JWT id to bloom filter |
GET | /me | Me | Decoded subject from the bearer token |
POST | /forgot-password | ForgotPassword | Issue a one-time reset token; emails it |
POST | /reset-password | ResetPassword | Consume reset token; update password hash |
GET | /check-username | CheckUsername | Availability check for the registration form |
GET | /check-email | CheckEmail | Availability 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
| Module | Relationship |
|---|---|
| Users | Shares the users table; auth writes credential columns, Users module writes profile columns. |
| Applications | The 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 var | Both servers? | Notes |
|---|---|---|
AUTH_JWT_SECRET | ✅ Must match | HMAC key for token signing/verification |
AUTH_JWT_EXPIRY | Auth only | Default 24h |
AUTH_JWT_REFRESH_EXPIRY | Auth only | Default 168h (7 days) |
AUTH_BCRYPT_COST | Auth only | Password-hash cost factor |
MICROSERVICE_AUTH_HOST | API only | Where the API finds the auth gRPC server |
API Reference
See the interactive API reference /auth tag for live examples.