Overview

The Go backend uses a stateless JWT authentication system combined with direct PostgreSQL queries to Better Auth tables. This architecture decouples identity management (handled by Better Auth on the Bun monolith) from resource authorization checks (handled by Go microservices). Key principle: Go backend is read-only with respect to user and organization data. All mutations (create user, assign roles, etc.) are performed via the Better Auth API on the Bun backend. Authentication flow

Core Components

1. Better Auth (Bun Monolith)

The source of truth for authentication and authorization. It manages:
  • User registration, login, OAuth, email verification
  • Platform-level roles (via admin plugin): user, admin, superadmin
  • Organization membership and roles (via organization plugin): owner, admin, staff, viewer
  • Issues signed JWTs containing:
    • userId (from users.id)
    • email
    • role (platform role from users.role)
    • organizationId (user’s active organization)
    • orgRole (user’s role in that organization)
    • isActive (derived from users.banned)
Database: PostgreSQL tables managed by Better Auth plugins (standardized plural names):
  • users — User identities and platform roles
  • accounts — OAuth provider data and password hashes
  • sessions — Active user sessions
  • members — Organization memberships and per-org roles
  • organizations — Organization entities
  • invitations — Pending organization invites
  • jwks — JWT key sets for signing

2. JWT Validation & Middleware

The Go backend validates incoming JWTs on every protected request:
  • Extraction: Reads Authorization: Bearer <token> header
  • Signature Verification: HS256 using shared BETTER_AUTH_SECRET (32+ chars, configured in both Bun and Go)
  • Claims Extraction: Parses userId, email, role, organizationId, orgRole from payload
  • Context Injection: Stores user data in Echo context for use in handlers
No database lookup is required for JWT validation — it’s stateless and cryptographically verified.

3. Redis Caching Strategy

Permission Cache (Organization-Level)

  • Key: perm:<userId>:<organizationId>
  • Value: { "role": "owner|admin|staff|viewer", "permissions": ["data:read", "data:write", "leave:approve", ...] }
  • TTL: 5 minutes (fallback)
  • Purpose: Cache role-to-permission resolution from the members table. Avoids repeated DB queries for authorization checks in every request.
  • Invalidation:
    • Real-time: When a user’s role or membership changes via the Better Auth API on Bun, an event is published to NATS. The Go backend’s MemberRoleChangeConsumer listens for these events and invalidates the corresponding cache keys immediately.
    • Time-based: 5-minute TTL as a safe fallback for consistency.
Note: JWT validation itself is NOT cached — it’s fast (local signature verification). Only authorization/permission checks are cached.

4. Direct Database Queries (No User Sync)

The architectural decision to eliminate data synchronization between microservices in favor of direct queries to a single source of truth (Better Auth PostgreSQL tables). Current approach:
  • Go backend queries users, members, and organizations tables directly.
  • No data duplication — single source of truth is Better Auth’s schema.
  • User lookups (repositories) now read from Better Auth tables.
  • Org role queries (permission service) read directly from members table.
Benefits:
  • Absolute Consistency: No synchronization lag or event processing delays.
  • Simplicity: Reduced infrastructure complexity (no NATS user sync topic).
  • Reduced Latency: Database joins are generally faster than distributed event processing and local duplication.

Authentication

  • Middleware: internal/api/http/middleware/auth_middleware.go — JWT extraction and validation
  • Validator: internal/api/http/middleware/better_auth_validator.go — Signature verification with Redis caching

User & Organization Data (Read-Only)

  • User Repository: internal/modules/user/adapter/outbound/persistence/postgresql/user_repository.go
    • FindByID() — Queries users table by ID
    • FindByEmail() — Queries users table by email
    • No write operations; all mutations happen via Better Auth API
  • Organization Repository: internal/modules/organizations/adapter/outbound/persistence/postgresql/organization_repository.go
    • GetByID() — Queries organizations table
    • GetBySlug() — Queries organizations table by slug
    • ListByUserID() — Joins members and organizations tables
    • No write operations

Authorization

  • Checks user permissions based on organization role
  • Caches permissions using Redis with 5-minute TTL
  • Queries members table for org roles

Event Consumers

  • Member Role Change Consumer: internal/modules/organizations/adapter/inbound/messaging/member_role_change_consumer.go
    • Listens for member role changes, additions, and removals via NATS
    • Invalidates permission cache in real-time

Summary

The Go backend’s authentication system is built on stateless verification and direct database access. This approach provides the best balance between performance (stateless JWT), consistency (no data sync), and scalability. Better Auth acts as the centralized authority, while Go remains a secure, high-performance consumer of identity information.