Bun Backend Authentication

This document describes the authentication and authorization architecture in the Bun-based backend monolith.

Overview

The authentication system is designed around Hexagonal Architecture principles, ensuring that the core business logic remains independent of the specific authentication provider. We use Better Auth as the primary authentication engine and JWT (JSON Web Tokens) for stateless session management across our API Gateway.

Architecture & Design Patterns

1. Ports (Interfaces)

Defined in @bun-core/auth-ports, the AuthPort interface specifies the contract for authentication operations. This abstraction allows the system to remain agnostic of the underlying implementation.
  • Primary Interface: AuthPort
  • Models: User, Session

2. Adapters (Implementation)

The @bun-core/auth-adapter package provides the concrete implementation of the AuthPort using Better Auth.
  • Better Auth Integration: Manages PostgreSQL tables (users, sessions, accounts, verifications, organizations, members, invitations, jwks) via Drizzle ORM.
  • Drizzle Adapter: Maps Better Auth models to our PostgreSQL schema using Drizzle’s type-safe query builder.
  • JWT Service: A standalone service within the adapter that handles:
    • Signing: Creates HS256 JWTs. During sign, enriches payload with user roles (from user_roles table) and organization context (from user_organizations table) fetched from the database.
    • Verification: Validates tokens using shared HMAC secret (BETTER_AUTH_SECRET).

3. API Gateway Middleware

The Hono-based API gateway uses specialized middleware to enforce security at the entry points.
  • Auth Middleware:
    • Extracts the Authorization: Bearer <token> header.
    • Uses JWTService.verify() to validate the token.
    • Attaches the decoded user payload to the Hono context (c.user).
  • Authorization Middleware:
    • requireRole(...roles): Checks the user’s roles attached to the context against the required roles for the endpoint.

Authentication Flow

Sign-In / Sign-Up Flow

  1. Request: User submits credentials (Email/Password or Social Provider) to the monolith (POST /api/auth/sign-in or POST /api/auth/sign-up).
  2. Better Auth Execution: Better Auth processes the request:
    • For email/password: Validates email exists, verifies password hash
    • For OAuth: Handles provider redirect, callback, and account linking
    • Creates or updates record in the auth table
    • Creates entry in sessions table for the new session
    • Creates accounts record with provider details (OAuth) or hashed password (email/password)
  3. Token Issuance: Once Better Auth validates the user, the JWTService is used to generate a stateless JWT.
    • Payload Enrichment: The JWT service queries the database to enrich the payload:
      • Fetches user metadata from auth table: email, emailVerified, isAnonymous, name
      • Fetches roles from user_roles table (all roles assigned to the user)
      • Fetches first organization from user_organizations table (if available)
      • Sets isActive: true as default (TODO: add isActive column to auth table if needed)
    • Token Format: HS256 signed JWT with payload containing all enriched user context
  4. Response: The JWT is returned to the client as response body and/or in a secure HTTP-only cookie (depending on flow).

Authenticated Request Flow

  1. Client Request: The client sends a request with the JWT in the Authorization: Bearer <token> header.
  2. Middleware Interception: The authMiddleware catches the request and extracts the token.
  3. Verification: JWTService.verify(token) checks the signature, expiration, and integrity using the shared secret.
  4. Payload Extraction: The JWT payload contains enriched user data:
    {
      userId: "auth_id_nanoid",
      email: "user@example.com",
      roles: ["user"],
      organizationId: "org_id",
      emailVerified: true,
      isAnonymous: false,
      isActive: true
    }
    
  5. Context Injection: The middleware attaches user info to Hono context:
    c.user = {
      userId: payload.userId,
      email: payload.email,
      roles: payload.roles
    }
    
    Note: organizationId and other fields are available in the full payload but not attached to c.user by default. Access via c.req.valid() or extend the middleware if needed.
  6. Role Check: If the endpoint is protected by requireRole('admin'), the requireRole middleware verifies if 'admin' is present in c.user.roles.
  7. Execution: If authorized, the request proceeds to the controller/handler.

Database Schema

The authentication system uses PostgreSQL tables managed by Drizzle ORM:
TablePurposeNotes
usersBetter Auth core user tableID (PK), email (unique), name, image, role, emailVerified, isAnonymous, timestamps
accountsOAuth and email/password credentialsStores provider tokens, refresh tokens, hashed passwords
sessionsActive user sessionsTracks session token, expiration, IP address, user agent
verificationsEmail verification codesStores verification tokens for email confirmation flow
organizationsOrganization entitiesStores organization data (name, timestamps)
membersUser-organization membershipTracks which users belong to which organizations with per-org roles (owner, admin, member, staff, viewer)
invitationsOrganization invitationsPending organization invitations with role and status
jwksJWT Key Set (Better Auth JWT plugin)Stores public/private keys for JWT signing

Components & File Mapping

ComponentPathPackageDescription
Auth Portports/auth.port.ts@bun-core/auth-portsMinimal interface definition for auth operations. Routes call authPort directly (no intermediate use cases).
Auth Adapterbetter-auth.adapter.ts@bun-core/auth-adapterThin bridge between Better Auth server API and route handlers. Delegates all operations to Better Auth.
JWT Servicejwt.service.ts@bun-core/auth-adapterHS256 signing and verification with DB enrichment (roles, org, user metadata).
Auth Middlewareapi-gateway/middleware/auth.middleware.tsMonolithHono middleware for Bearer token extraction and context mapping.
Auth Schemaschema.ts@bun-core/auth-adapterDatabase schema for Better Auth tables using Drizzle ORM.
Auth Contextauth-context.ts@bun-core/auth-adapterAttaches authenticated user data to request context.
Custom use cases removed: RegisterUserUseCase, LoginUserUseCase, UpdateUserProfileUseCase, GetUserUseCase, and UserRepository (MongoDB) have been deleted. Route handlers now call authPort directly, delegating to Better Auth APIs. See Better Auth User Module Consolidation for details.

Technical Specifications

  • Token Algorithm: HS256 with HMAC-SHA256
  • Token Library: jose (Node.js/Bun compatible)
  • Runtime: Bun
  • Database ORM: Drizzle
  • Database: PostgreSQL
  • Token Verification: Stateless (via shared secret key in BETTER_AUTH_SECRET)
  • Role Mapping: Persistent in user_roles table, enriched into JWT payload at signing time
  • Organization Mapping: Optional per-user organization in user_organizations table, included in JWT
  • Password Hashing: Better Auth default (scrypt) — custom password logic removed
  • Session Duration: Configurable via JWT expiration time (expiresIn parameter)

Security Considerations

  1. Secret Key Management:
    • BETTER_AUTH_SECRET must be cryptographically random (32+ characters)
    • Keep secret server-side only, never expose to frontend
    • Rotate if compromised
  2. Token Validation:
    • All tokens verified server-side via signature check
    • Expiration time checked automatically by jose library
    • No whitelist required (stateless verification)
  3. Role Enrichment:
    • Roles are queried from database at signing time, not stored in user input
    • Frontend receives roles in JWT but cannot modify them
    • Role changes reflected in next token issuance
  4. Cross-Origin Requests:
    • JWT sent as Bearer token in Authorization header (suitable for CORS)
    • Alternative: HTTP-only secure cookie (suitable for same-origin requests)
    • Configure based on frontend deployment location

Known Limitations & Future Improvements

  1. Missing isActive Column:
    • Currently hardcoded to true in JWT enrichment
    • TODO: Add is_active boolean column to auth table for user deactivation support
  2. Single Organization Context:
    • JWT includes only first organization from user_organizations
    • TODO: Support multiple organizations per user with context switching
  3. Role Caching:
    • Roles queried from DB at every token issuance (not cached)
    • TODO: Consider caching with TTL if role assignment frequency is high
  4. Email Verification:
    • Email verification flow managed by Better Auth
    • Currently not enforced during sign-up
    • TODO: Implement optional email verification gate
  5. Password Reset Flow:
    • Not yet implemented
    • TODO: Add forgot password + reset token flow