ADR-0006: Go backend as read-only consumer of Better Auth via shared PostgreSQL
Status
AcceptedTags
authentication, better-auth, postgresql, multi-tenancy, permissions, redis, natsDecision
Go microservices skip a synchronization layer and query the Better Auth PostgreSQL tables directly using read-onlySELECT operations. Go models must exactly match the schema generated by the Better Auth plugins. Cache invalidation is handled in real-time via NATS events.
Why
Adding a NATS sync layer to replicate Better Auth identity data into Go-owned tables introduces synchronization lag, dual-write complexity, and a second source of truth. Direct queries to a single shared schema are simpler, strictly consistent, and lower latency.Table ownership
| Table | Owner | Go access |
|---|---|---|
users | Better Auth core + Admin plugin | SELECT only |
members | Organization plugin | SELECT only |
organizations | Organization plugin | SELECT only |
accounts | Better Auth core | No access needed |
Permission resolution (multi-tier)
- JWT claims (
orgRole,organizationId) — fast, no IO, used for coarse checks - Redis cache (
perm:<userId>:<orgId>, 5m TTL) — mid-tier, avoids repeated DB hits - PostgreSQL (
memberstable) — authoritative, queried only on cache miss
Consistency model
Role/membership changes in Bun trigger NATS events (member.role.changed, member.removed). The Go MemberRoleChangeConsumer immediately deletes the affected Redis cache key. The next request re-queries PostgreSQL. Sub-second consistency without synchronization overhead.
Required indexes
Rules for agents
- Go models that map to Better Auth tables must use exact plural table names:
users,members,organizations - Never add write operations to Better Auth table repositories —
INSERT,UPDATE,DELETEbelong in the Bun backend only - Role changes must go through the Better Auth API on Bun, never direct SQL from Go
- Maintain all three required indexes; do not drop them
- When adding a new permission check, follow the three-tier lookup: JWT claim → Redis → PostgreSQL