Authentication Flow with Better Auth Plugins
This document describes the complete authentication and authorization flow across the Gremlin system with the Better Auth plugin architecture.This document covers how you sign in and how tokens are minted and verified.
For which module a signed-in user can reach, see
Module Access Control.
System Architecture Overview
- Bun owns every auth write. Go reads the auth tables directly but never writes them (ADR-0006). All role mutations go through the Better Auth API on Bun.
- Two secrets, two token types. The app-JWT is HS256 signed with
BETTER_AUTH_SECRET, shared by Bun and Go. The PowerSync sync token is asymmetric, signed with a private key from thejwkstable, so PowerSync only ever receives public keys. - Redis is the revocation channel, not a permission cache. Both backends read the same
authInvalidBefore:<userId>marker. - Rental bypasses Bun.
/api/rental/*is proxied by the SolidStart server straight to Go, forwarding the session cookie with Better Auth’s HMAC suffix stripped. - Mobile never calls Go directly. The app has a single
API_BASE_URLpointing at Bun; every Go-owned mobile path (/api/mobile/sync/*, the fieldforce routes) needs an explicit proxy route inapi-gateway/routes/index.tsor the app gets a misleading 404. Go re-validates the JWT behind that proxy.
1. User Registration Flow
Step-by-Step Process
1.1 User Initiates Sign-Up
1.2 Bun Backend Processes Registration
1.3 Frontend Receives the Session
2. User Login Flow
Step-by-Step Process
2.1 User Initiates Sign-In
2.2 Bun Backend Verifies Credentials
2.3 Frontend Receives the Session
3. App-Token Exchange (Session → JWT)
Clients that cannot rely on cookies — the mobile app, and any caller that needs a bearer token for the Go backend — exchange their live session for an HS256 app-JWT.Two different tokens exist. Do not confuse them.
The sync token is short-lived and comes from Better Auth’s jwt plugin
(
/api/auth/token + /api/auth/jwks), so PowerSync receives verify-only public keys and
never the shared secret.buildRoles is re-implemented on both sides of the trust boundary — Bun
(services/auth/api.ts) and Go (internal/api/http/middleware/better_auth_validator.go).
The behaviour is pinned by contracts/auth/role-revocation-vectors.json; change one side
without the other and TestAuthGoldenVectors (Go) or auth-golden.test.ts (Bun) fails.
4. Authenticated Request Flow (Go Backend)
Step-by-Step Process
4.1 Frontend Makes Authenticated Request
4.2 Bun Backend Verifies JWT (if applicable)
4.3 Go Backend Processes Request
5. Organization Management Flow
5.1 Create Organization
5.2 Invite Member to Organization
members row is created only when the invitation is accepted — an invitation and a
membership are separate records. Related routes on the same mount:
5.3 Change a Member’s Role
6. Permission Query Flow (Go Backend)
6.1 Authorization for Protected Operations
6.2 Cache Invalidation Flow
Invalidation happens inside the Bun backend, at the point of the write:7. Role Hierarchy & Permissions
Platform Roles (Better Auth Admin Plugin)
Organization Roles (Better Auth Organization Plugin)
Stored in themembers.role field, per (userId, organizationId). The permission set is
resolved from defaultRolePermissions in permission_service.go — permissions are not
stored in a table.
field_team is a further org role used by the fieldforce module. It is not in
defaultRolePermissions — it is checked inline in fieldforce handlers to restrict field
workers from briefings and risk data. Organizations can also define custom roles
(custom_roles table, referenced by members.custom_role_ids).
For how these roles map to module access, see
Module Access Control.
8. Error Handling
Common Error Scenarios
9. Security Measures
App-JWT Security
- Signature: HS256 using the shared
BETTER_AUTH_SECRET - Expiration: 7 days (
APP_TOKEN_TTLinservices/auth/api.ts) - Storage: HttpOnly cookie for web; secure storage on mobile
- Transport: HTTPS only (enforced in production)
Token Revocation
A long TTL is safe because immediacy comes from server-side revocation, not a short expiry (services/auth/revocation.ts):
- On any status, role, or permission change, a per-user marker
authInvalidBefore:<userId> = nowis written to Redis. - Both the Bun middleware and Go’s
better_auth_validator.goreject any JWT whoseiatpredates the marker. Every outstanding token for that user dies at once. - The marker TTL is 8 days — one day longer than the longest-lived token — so it self-expires without unbounded growth.
- A companion
auth_versionstable tracks a per-user version counter.
Password Security
- Hashing: scrypt (Better Auth default), stored in
accounts.password - Storage: hashed only, never plaintext
- Comparison: constant-time comparison to prevent timing attacks
- Minimum length: 8 characters (Better Auth default)
Database Security
- Access Control: PostgreSQL user permissions restrict access
- Connection: SSL/TLS encryption in production
- Secrets: Environment variables, not hardcoded
Multi-Tenancy
- Isolation: Organization data accessed via organizationId in context
- RBAC: Role-based access control at organization level
- Audit: Member actions logged for compliance
10. Migration from Old Auth System
Key Changes
- No NATS User Sync: Previously, users were synced via NATS events. Now they’re queried directly from PostgreSQL.
- No Local User Table (Go): Go backend no longer creates/maintains its own users table.
- Direct Query Pattern: Go backend queries Better Auth tables (users, members, organizations).
- Centralized Mutations: All write operations go through Better Auth API on Bun.
For Backend Developers
- Use
PermissionServicefor authorization checks - Query users/orgs using the provided repositories
- Role changes reach Go through token revocation plus the 5-minute cache TTL — there is no NATS invalidation (see Section 6.2)
- Permission checks are fast (cached, with a < 50ms latency target)
For Frontend Developers
- Update user/org creation calls to use Better Auth API
- Read operations remain unchanged (Go backend still provides queries)
- Handle new permission error responses (403)
11. Monitoring & Troubleshooting
Key Metrics to Monitor
- Auth latency: time to verify a JWT and fetch user data
- Cache hit rate: permission cache effectiveness
- Redis availability: the revocation marker store — Bun fails closed without it
- Database query performance: user/org queries should be < 50ms
Common Issues
12. API Endpoints Summary
Authentication (Bun)
User Management (Bun - Admin Plugin)
Organization Self-Service (Bun — services/organizations/api.ts)
Mounted at /api/v1/organizations. Requires an authenticated org member.
createRolesAPI) and teams (createTeamsAPI) mount on the same prefix.
Organization creation goes through the Better Auth organization plugin, served by the
catch-all /api/auth/* handler.
Platform Admin (Bun — services/admin/api.ts)
Mounted at /api/v1/admin/organizations, every route behind adminOnlyMiddleware.
Full CRUD over any organization, its members, custom roles, invitations, and audit log.
Rental Platform Role (Go)
User & Organization Queries (Go)
References
- Module Access Control — which user reaches which module
- Bun Backend Authentication
- Go Backend User Module
- Go Backend Organization Module
- Permission Service Integration
- CLI Auth Architecture — device flow and API keys
- ADR-0006 — Better Auth read-only consumer (Go never writes roles)