ADR-0005: Stateless JWT authentication with direct PostgreSQL reads
Status
AcceptedTags
authentication, jwt, better-auth, postgresql, redis, security, multi-tenancyDecision
The Go backend validates JWTs using a sharedBETTER_AUTH_SECRET (HS256, stateless — no DB lookup on every request). Authorization checks query the Better Auth PostgreSQL tables directly. Permission results are cached in Redis with a 5-minute TTL and invalidated in real-time via NATS when roles change.
Why
The Go backend is a read-only consumer of identity data managed by the Bun monolith (Better Auth). Two alternatives were rejected:- NATS user sync: replicating user/org data into Go-owned tables adds synchronization lag, dual-write complexity, and eventual consistency risk. Rejected.
- Per-request API call to Bun: adds network round-trip on every protected request. Rejected.
Flow
Key constraints
- Go is read-only on Better Auth tables (
users,members,organizations,accounts) - All mutations (create user, assign role, invite member) go through the Bun backend’s Better Auth API
- JWT is NOT cached — signature verification is local and fast
- Only permission resolution (role → permissions mapping) is cached
Rules for agents
- Never write to
users,members, ororganizationstables from Go — SELECT only - JWT validation must use
BETTER_AUTH_SECRET(HS256) — do not implement a separate key management system - Permission cache key format:
perm:<userId>:<organizationId> - On
member.role.changedormember.removedNATS events, invalidate the corresponding cache key immediately - Use the
memberstable for org role checks, not the JWT claim alone — claims may be stale if the role changed after login - Required indexes:
idx_users_role,idx_members_user_org,idx_members_organization_id_role