Auth Architecture
Strategy
Build auth in-house for MVP as an isolated module in the server crate. Design the boundary so it can be swapped for a self-hostable identity provider (Zitadel is the current front-runner) when advanced features are needed.
What the Auth Module Owns (MVP)
- Email + password registration and login
- Password hashing (argon2)
- JWT access tokens (short-lived, e.g., 15 minutes) + refresh tokens (long-lived, e.g., 30 days)
- Password reset via email
- Email verification
- Placeholder user invite flow (walk-on upgrade)
What the Auth Module Produces
Every authenticated request results in a validated context:
struct AuthContext {
user_id: UserId,
org_id: OrgId,
role: OrgRole, // admin | instructor | ro | member
}Everything downstream of this — authorization checks, business logic, API handlers — consumes AuthContext and never touches passwords, tokens, or session management directly. This is the interface boundary that enables a future swap.
Token Strategy
Short-lived JWTs + refresh tokens. The JWT carries the user’s identity and can be validated without hitting the database on every request. The refresh token is checked server-side and used to issue new JWTs when the access token expires.
Offline support: Mobile apps carry a valid JWT for local operations. When the JWT expires offline, scores are queued locally — no server calls needed. When connectivity returns, the app uses the refresh token to get a new JWT, then flushes the score queue.
Revocation: Refresh tokens are stored server-side and can be revoked (e.g., when a user is removed from an org). The short JWT lifetime limits the window of stale access.
Server Crate Structure
Auth is a contained module, not scattered:
apps/server/
src/
auth/ # registration, login, JWT, password reset, invite flow
handlers/ # API route handlers (consume AuthContext)
...
The auth/ module is the only code that touches passwords, tokens, and session state. If swapped for an external provider, only this module changes.
Auth emails (verification, password reset, invites) are sent via SMTP using whatever email provider the deployment is configured with. No dependency on a specific provider — the SMTP connection is configuration, not code.
Migrated Users
Existing users migrated from Supabase Auth (see Migration Strategy) will not have credentials in the new auth module — their password_hash is null.
Login Flow for Migrated Users
- User enters email + password on the login screen
- Server detects the account has
password_hash IS NULL - Server returns a
FAILED_PRECONDITIONerror with a server-defined message: “No account found for this email, or a password reset is required. Check your email for a reset link.” - Server silently sends a password reset email to the address (if the account exists)
- App displays the server’s error message as-is — no hardcoded migration-specific copy in the client
- User clicks the reset link, sets a new password, logs in normally
The ambiguous error message prevents email enumeration — an attacker cannot determine whether an email is registered. The same message is returned for a genuinely nonexistent email (with no reset email sent).
This reuses the standard password reset infrastructure. No migration-specific code in the clients — when migration is complete and all users have set passwords, the server-side null check becomes a no-op with no app update required.
What This Does NOT Cover (MVP)
These features are not needed for the MVP (church security teams) but are anticipated for later customer segments:
- MFA / 2FA (TOTP, passkeys, WebAuthn) — needed for LE/military
- OIDC / OAuth2 federation — lets PDs use their existing Active Directory
- LDAP integration — LE/military identity infrastructure
- Social logins (Sign in with Apple/Google) — nice-to-have for mobile onboarding
- Brute force protection / account lockout — basic rate limiting at MVP, advanced policies later
- FIPS/CJIS compliance — specific cryptographic and audit requirements for LE
- Audit logging — event-sourced auth logs for compliance
Future: Zitadel Migration Path
When advanced auth features are needed (likely when targeting LE or military segments), the auth module can be replaced with Zitadel — a self-hostable, Go-based identity provider with built-in multi-tenancy, MFA, OIDC federation, LDAP, and per-org branding.
Why Zitadel
- Multi-tenancy built in — Instance → Organization → Project → Application hierarchy maps to our Org model
- Single Go binary — lightweight, easy to deploy for white-label/on-prem
- Per-org branding and security policies — each org can have its own login page, MFA requirements
- Event-sourced audit logs — natural fit for LE compliance
- Custom SMTP — uses whatever email provider the deployment has
What the swap looks like
- Deploy Zitadel alongside the Axum server
- Replace the
auth/module — registration, login, token issuance move to Zitadel - The server becomes a Zitadel consumer: validate tokens, read user identity from Zitadel’s OIDC claims
AuthContextstays the same — nothing downstream changes- Existing users are migrated into Zitadel (one-time import)
Other evaluated providers
- Ory (Kratos + Hydra) — modular, API-first, more assembly required
- Keycloak — most mature, but heavy (Java), complex to operate, overkill for our deployment model
- Authentik — flexible flows, heavier resource usage, Docker Compose required
- Rauthy — Rust-native, very lightweight, but smaller community and less multi-tenancy depth