Authentication Flow with Better Auth Plugins

This document describes the complete authentication and authorization flow across the Gremlin system with the new Better Auth plugin architecture.

System Architecture Overview

System Architecture Overview

1. User Registration Flow

Step-by-Step Process

1.1 User Initiates Sign-Up

Frontend (Astro / SolidStart)

  ├─ User enters: email, password, name


POST /api/auth/sign-up
{
  "email": "user@example.com",
  "password": "secure_password_123",
  "name": "John Doe"
}

1.2 Bun Backend Processes Registration

Bun Backend (Better Auth)

  ├─ Validate email format
  ├─ Check for duplicate email
  ├─ Hash password using scrypt

  ├─ Create "users" table entry
  │  └─ id: nanoid (26 chars)
  │  └─ email: user@example.com
  │  └─ name: John Doe
  │  └─ emailVerified: false
  │  └─ isAnonymous: false
  │  └─ role: "user" (default)

  ├─ Create "accounts" table entry (password hash)
  │  └─ userId: nanoid_26
  │  └─ password: $2b$12$... (scrypt hash)
  │  └─ provider: "credential"

  ├─ Create "sessions" table entry
  │  └─ userId: nanoid_26
  │  └─ sessionToken: random_token
  │  └─ expiresAt: 30 days from now

  ├─ Issue JWT Token
  │  ├─ Fetch user data: id, email, name, role
  │  ├─ Fetch user roles from "users.role" field
  │  ├─ Sign with HS256 using BETTER_AUTH_SECRET
  │  │
  │  └─ JWT Payload:
  │     {
  │       "userId": "nanoid_26",
  │       "email": "user@example.com",
  │       "name": "John Doe",
  │       "role": "user",
  │       "emailVerified": false,
  │       "isAnonymous": false,
  │       "iat": 1713265800,
  │       "exp": 1713352200
  │     }


Return JWT to Frontend

1.3 Frontend Receives Token

Frontend

  ├─ Store JWT in:
  │  ├─ HttpOnly Cookie (secure)
  │  ├─ or localStorage (less secure)
  │  ├─ or sessionStorage (temporary)

  ├─ Redirect to dashboard


User authenticated

2. User Login Flow

Step-by-Step Process

2.1 User Initiates Sign-In

Frontend (Astro / SolidStart)

  ├─ User enters: email, password


POST /api/auth/sign-in
{
  "email": "user@example.com",
  "password": "secure_password_123"
}

2.2 Bun Backend Verifies Credentials

Bun Backend (Better Auth)

  ├─ Lookup user by email in "users" table

  ├─ Fetch password hash from "accounts" table

  ├─ Compare provided password with stored hash
  │  ├─ If match: proceed
  │  ├─ If no match: return 401 Unauthorized

  ├─ Create/Update "sessions" table entry
  │  └─ sessionToken: new random token
  │  └─ expiresAt: 30 days from now

  ├─ Issue JWT Token (same as sign-up)
  │  └─ Signed with BETTER_AUTH_SECRET
  │  └─ Contains: userId, email, role, etc.


Return JWT to Frontend

2.3 Frontend Receives Token

Frontend

  ├─ Store JWT (same as sign-up)
  ├─ Redirect to dashboard


User authenticated

3. Authenticated Request Flow (Go Backend)

Step-by-Step Process

3.1 Frontend Makes Authenticated Request

Frontend (Astro / SolidStart)

  ├─ Include JWT in Authorization header


GET /api/v1/users/me
Authorization: Bearer eyJhbGc... (JWT)

3.2 Bun Backend Verifies JWT (if applicable)

Note: If API Gateway is Bun-based, verification happens here
Bun Backend API Gateway

  ├─ Extract JWT from Authorization header
  ├─ Verify signature using BETTER_AUTH_SECRET
  ├─ Check token expiration
  ├─ Attach decoded payload to request context


Forward to appropriate backend service

3.3 Go Backend Processes Request

Go Backend (api/v1)

  ├─ Extract JWT from Authorization header

  ├─ Verify JWT signature using BETTER_AUTH_SECRET
  │  (Same secret as Bun, ensures trust)

  ├─ Extract userId from JWT claims

  ├─ Query user from PostgreSQL
  │  SELECT * FROM "users" WHERE id = ?

  ├─ Return user data
  │  {
  │    "id": "nanoid_26",
  │    "email": "user@example.com",
  │    "name": "John Doe",
  │    "role": "user",
  │    "emailVerified": false,
  │    "isAnonymous": false,
  │    "image": null,
  │    "createdAt": "2024-04-16T10:30:00Z",
  │    "updatedAt": "2024-04-16T10:30:00Z"
  │  }


Frontend receives user data

4. Organization Management Flow

4.1 Create Organization

Frontend

  ├─ Authenticated user clicks "Create Organization"


POST /api/auth/organization/create
Authorization: Bearer <JWT>
{
  "name": "Acme Corp",
  "slug": "acme-corp"
}

Bun Backend (Organization Plugin)

  ├─ Verify JWT and extract userId

  ├─ Validate organization name/slug

  ├─ Create "organizations" table entry
  │  └─ id: nanoid_26
  │  └─ name: Acme Corp
  │  └─ slug: acme-corp (unique)
  │  └─ creatorId: userId (from JWT)
  │  └─ createdAt: now

  ├─ Create "members" table entry (creator as owner)
  │  └─ userId: nanoid_26
  │  └─ organizationId: nanoid_26
  │  └─ role: "owner"
  │  └─ joinedAt: now


Return organization data with member role

4.2 Invite Member to Organization

Frontend

  ├─ Org admin clicks "Invite Member"
  │ ├─ Enters email: "member@example.com"
  │ ├─ Selects role: "staff"


POST /api/auth/organization/:id/members/invite
Authorization: Bearer <JWT>
{
  "email": "member@example.com",
  "role": "staff"
}

Bun Backend (Organization Plugin)

  ├─ Verify JWT and extract userId

  ├─ Check if userId is org member with permission to invite

  ├─ Look up user by email (or create invitation)

  ├─ Create "members" table entry
  │  └─ userId: email_based_or_existing
  │  └─ organizationId: org_id
  │  └─ role: "staff"
  │  └─ status: "pending" (if new)

  ├─ Create/Update "invitations" table entry

  ├─ Notify user (email) of invitation


Return member data with status

4.3 Accept Invitation (Optional Flow)

Frontend

  ├─ User clicks "Accept Invitation" in email


POST /api/auth/organization/:id/members/accept
{
  "invitationToken": "token_from_email"
}

Bun Backend

  ├─ Verify invitation token

  ├─ Mark member status as "active"

  ├─ Delete invitation record


Member now active in organization

5. Permission Query Flow (Go Backend)

Authorization for Protected Operations

Frontend wants to approve a leave request

  ├─ User makes request to Go backend


POST /api/v1/leave/requests/:id/approve
Authorization: Bearer <JWT>
{
  "approverId": "user_id"
}

Go Backend

  ├─ Extract userId from JWT
  ├─ Extract organizationId from request context

  ├─ Call PermissionService.CanUserApproveLease(
  │    userId, organizationId
  │  )

  ├─ PermissionService checks:
  │  ├─ Query members table:
  │  │  SELECT role FROM members
  │  │  WHERE user_id = ? AND organization_id = ?
  │  │
  │  ├─ Check if role in ["owner", "admin", "staff"]
  │  │
  │  ├─ If not cached, query defaults
  │  │
  │  ├─ Cache result in Redis for 5 minutes

  ├─ If authorized: proceed with approval
  ├─ If not authorized: return 403 Forbidden


Return result

Cache Invalidation Flow

Org admin changes a member's role (via Bun backend)

Bun Backend

  ├─ Update members table:
  │  UPDATE members SET role = 'admin' WHERE id = ?

  ├─ Publish NATS event: "member.role.changed"
  │  {
  │    "userId": "nanoid_26",
  │    "organizationId": "nanoid_26",
  │    "oldRole": "staff",
  │    "newRole": "admin",
  │    "timestamp": 1713265800
  │  }


NATS Server publishes to "member.role.changed"

Go Backend

  ├─ Subscribe to NATS member.role.changed

  ├─ MemberRoleChangeConsumer receives event

  ├─ Invalidate Redis cache:
  │  DEL permission_cache:userId:organizationId

  ├─ On next permission query, freshly query database


Permission checks now use updated role

6. Role Hierarchy & Permissions

Platform Roles (Better Auth Admin Plugin)

Stored in: "users".role field

Roles:
├─ superadmin  → System administrator
├─ admin       → Platform administrator
└─ user        → Regular user

Organization Roles (Better Auth Organization Plugin)

Stored in: "members".role field per (userId, organizationId)

Roles:
├─ owner   → Full control of organization
├─ admin   → Can manage members and approvals
├─ staff   → Can approve leaves, manage basic operations
├─ member  → Regular member
└─ viewer  → Read-only access

Permission Mapping:
├─ owner/admin
│  ├─ leave:approve         (approve leave requests)
│  ├─ member:invite         (invite new members)
│  ├─ member:update         (change member roles)
│  ├─ member:remove         (remove members)
│  └─ org:manage            (manage organization settings)

├─ staff
│  ├─ leave:approve         (approve leave requests)
│  └─ data:read             (read organization data)

├─ member
│  ├─ data:read             (read organization data)
│  └─ leave:request         (submit leave requests)

└─ viewer
   └─ data:read             (read-only access)

7. Error Handling

Common Error Scenarios

ScenarioStatus CodeResponse
Missing/Invalid JWT401{ "error": "Unauthorized", "message": "Invalid token" }
Expired JWT401{ "error": "Unauthorized", "message": "Token expired" }
Insufficient permissions403{ "error": "Forbidden", "message": "Insufficient permissions" }
User not found404{ "error": "Not Found", "message": "User not found" }
Organization not found404{ "error": "Not Found", "message": "Organization not found" }
Duplicate email409{ "error": "Conflict", "message": "Email already exists" }
Invalid request400{ "error": "Bad Request", "message": "Invalid input" }
Server error500{ "error": "Internal Server Error", "message": "Something went wrong" }

8. Security Measures

JWT Security

  • Signature: HS256 using shared BETTER_AUTH_SECRET
  • Expiration: Typically 24 hours
  • Storage: HttpOnly Cookie (recommended) or secure storage
  • Transport: HTTPS only (enforced in production)

Password Security

  • Hashing: scrypt with salt rounds
  • Storage: Hashed passwords in accounts table, never plaintext
  • Comparison: Constant-time comparison to prevent timing attacks

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

9. Migration from Old Auth System

Key Changes

  1. No NATS User Sync: Previously, users were synced via NATS events. Now they’re queried directly from PostgreSQL.
  2. No Local User Table (Go): Go backend no longer creates/maintains its own users table.
  3. Direct Query Pattern: Go backend queries Better Auth tables (users, members, organizations).
  4. Centralized Mutations: All write operations go through Better Auth API on Bun.

For Backend Developers

  • Use PermissionService for authorization checks
  • Query users/orgs using provided repositories
  • Cache invalidation happens via NATS events automatically
  • Permission checks are fast (cached, with < 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)

10. Monitoring & Troubleshooting

Key Metrics to Monitor

  • Auth latency: Time to verify JWT and fetch user data
  • Cache hit rate: Permission cache effectiveness
  • NATS connection: Event delivery for cache invalidation
  • Database query performance: User/org queries should be < 50ms

Common Issues

IssueCauseSolution
”Token expired” errorsJWT expires after 24hImplement token refresh flow
”Unauthorized” on valid tokenToken signature mismatchVerify BETTER_AUTH_SECRET matches Bun
Stale permissionsCache not invalidatedCheck NATS connection and member.role.changed events
Slow permission checksCache miss, many database hitsCheck Redis connection and cache TTL
”User not found” after signupDatabase replication lagAdd retry logic with exponential backoff

11. API Endpoints Summary

Authentication (Bun)

POST   /api/auth/sign-up              Sign up new user
POST   /api/auth/sign-in              Sign in with email/password
POST   /api/auth/sign-out             Sign out current user
POST   /api/auth/refresh-session      Refresh JWT token

User Management (Bun - Admin Plugin)

POST   /api/auth/admin/create-user    Create new user
POST   /api/auth/admin/set-role       Change user's platform role
GET    /api/auth/admin/list-users     List all users
GET    /api/auth/admin/get-user       Get user details
POST   /api/auth/admin/remove-user    Delete user

Organization Management (Bun - Organization Plugin)

POST   /api/auth/organization/create                Create organization
PATCH  /api/auth/organization/:id                   Update organization
DELETE /api/auth/organization/:id                   Delete organization
POST   /api/auth/organization/:id/members/invite    Invite member
PATCH  /api/auth/organization/:id/members/:userId/update    Update member role
DELETE /api/auth/organization/:id/members/:userId           Remove member

User & Organization Queries (Go)

GET    /api/v1/users/:id                     Get user by ID (read-only)
GET    /api/v1/organizations/me               Get current org (read-only)
GET    /api/v1/organizations/me/members       List org members (read-only)

References