How Thunderbolt's Backend Authentication System Works with Better Auth

Thunderbolt implements a password-less backend authentication system using Better Auth with a custom factory that configures OTP-based sign-in, PostgreSQL persistence via Drizzle, and hooks for waitlist validation and cleanup.

The Thunderbolt email client (thunderbird/thunderbolt) uses a sophisticated backend authentication system that eliminates passwords entirely. This implementation leverages the Better Auth library integrated with PostgreSQL through Drizzle ORM to handle OTP-based sign-in flows. The architecture combines rate limiting, device binding, and optional OIDC support to create a secure, scalable authentication layer.

Better Auth Factory Configuration

The core of Thunderbolt's backend authentication system resides in the createAuth factory function located in backend/src/auth/auth.ts. This function initializes the Better Auth instance with PostgreSQL support and custom plugins.

Database Adapter and Schema

The factory connects Better Auth to the existing Drizzle ORM setup using the drizzleAdapter:

betterAuth({
  database: drizzleAdapter(database, { 
    provider: 'pg', 
    schema 
  })
})

This configuration stores verification rows, sessions, and user data in PostgreSQL while respecting the existing schema definitions in backend/src/dal/otp-challenge.ts and related data access layers.

Trusted Origins and Security Headers

The factory parses process.env.TRUSTED_ORIGINS to configure CORS and validate magic-link URLs. It also configures IP address headers for proxy environments:

advanced: {
  ipAddress: {
    ipAddressHeaders: getTrustedIpHeaders(settings.trustedProxy)
  }
}

This ensures accurate client IP detection when running behind Cloudflare or Akamai, which feeds into rate-limiting decisions.

Rate Limiting Configuration

The backend authentication system implements two layers of protection against brute-force attacks:

  1. Global rate limit: rateLimit: { enabled: true, window: 60, max: 10 } restricts the OTP endpoint to 10 requests per minute per IP address.
  2. Per-challenge limits: The emailOTP plugin enforces allowedAttempts: 3, returning TOO_MANY_ATTEMPTS after three failed OTP entries.

OTP Authentication Flow

Thunderbolt's password-less flow relies on email-based one-time passwords coordinated through Better Auth plugins and custom middleware hooks.

Challenge Token Generation

When a user initiates sign-in, the system creates a cryptographically secure challenge token stored in backend/src/dal/otp-challenge.ts. The getOrCreateOtpChallenge function implements a first-writer-wins strategy to prevent race conditions.

The challenge token binds the OTP request to a specific sign-in attempt, preventing replay attacks across different devices or sessions.

Before Hook Validation

The createAuth factory registers a before hook (via createAuthMiddleware) that executes before Better Auth processes the OTP verification:

  1. Validates the request path matches the OTP endpoint
  2. Extracts the x-challenge-token header (defined in backend/src/auth/otp-constants.ts)
  3. Calls validateOtpChallenge to confirm the token is non-expired and matches the email
  4. For new users, checks the waitlist status via isAutoApprovedDomain or ensures manual approval

If validation fails, the hook throws UNAUTHORIZED before the OTP reaches Better Auth's verification logic.

After Hook Cleanup

Upon successful authentication, the after hook performs critical cleanup:

  • deleteOtpChallengesForEmail: Removes all challenge tokens for the authenticated email
  • deletePersistedSignInOtp: Cleans up the verification row stored by Better Auth's emailOTP plugin
  • markUserNotNew: Updates the isNew flag in user.additionalFields to disable first-login flows

This ensures that OTPs remain single-use and session state stays consistent.

Waitlist and Access Control

The backend authentication system integrates with Thunderbolt's waitlist feature to gate access. Located primarily in backend/src/waitlist/utils.ts, this system:

  • Automatically approves users from specific domains via isAutoApprovedDomain
  • Adds new users to the waitlist with createWaitlistEntry if not auto-approved
  • Sends "waitlist-joined" or "not-ready" emails instead of OTPs when access is pending

The before hook enforces these restrictions, ensuring only approved users can complete the OTP flow and create sessions.

Optional OIDC Integration

When settings.authMode === 'oidc', the factory invokes buildOidcPlugins() to add OpenID Connect support. This returns a genericOAuth plugin configured with:

  • Issuer URL, client ID, and client secret from environment variables
  • Scopes for profile and email access
  • Redirect URL pointing to ${settings.betterAuthUrl}/v1/api/auth/oauth2/callback/oidc

This allows the backend authentication system to support hybrid authentication, accommodating both password-less OTP for individual users and OIDC for enterprise environments.

Session and Device Binding

The createAuth factory configures additional session fields to enhance security:

session: {
  additionalFields: {
    deviceId: { type: 'string', required: false }
  }
}

The client supplies a deviceId during authentication, which binds the session to a specific device. Combined with the challenge token mechanism, this prevents cross-device session theft and replay attacks.

Code Examples

Initialize Better Auth in the Server

import { createAuth } from '@/auth/auth';
import { db } from '@/db/client';

// Called when the backend starts
export const auth = createAuth(db);

Trigger OTP Send (Client Side)

await fetch(`${API_URL}/sign-in/email-otp`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: userEmail }),
});

Better Auth invokes the emailOTP.sendVerificationOTP plugin, which calls sendSignInEmail with the OTP and magic link generated via buildVerifyUrl from backend/src/auth/utils.ts.

Complete Sign-in with OTP

const response = await fetch(`${API_URL}/sign-in/email-otp`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-challenge-token': challengeToken, // from the previous step
  },
  body: JSON.stringify({ email: userEmail, otp: enteredOtp }),
});

const { session, user } = await response.json();
localStorage.setItem('authToken', session.accessToken);

The before hook validates the challenge token using validateOtpChallenge from backend/src/dal/otp-challenge.ts, while the after hook cleans up used tokens and marks the user as not new.

Using Bearer Token for API Calls

const token = localStorage.getItem('authToken');
await fetch(PROTECTED_ENDPOINT, {
  headers: { Authorization: `Bearer ${token}` },
});

The bearer plugin configured with requireSignature: true enforces signed JWTs for mobile client requests.

Key Implementation Files

File Purpose
backend/src/auth/auth.ts Core createAuth factory, Better Auth configuration, plugins, and hooks
backend/src/dal/otp-challenge.ts Challenge token storage with getOrCreateOtpChallenge, validateOtpChallenge, and cleanup functions
backend/src/auth/otp-constants.ts Constants for OTP expiry durations and the x-challenge-token header name
backend/src/auth/utils.ts Utility functions including buildVerifyUrl for magic links and getValidatedOrigin for CORS validation
backend/src/waitlist/utils.ts Waitlist logic including isAutoApprovedDomain and email notification helpers

Summary

  • Thunderbolt's backend authentication system uses a custom createAuth factory in backend/src/auth/auth.ts to configure Better Auth with PostgreSQL via Drizzle ORM.
  • The system implements password-less sign-in through email OTPs with challenge tokens stored in backend/src/dal/otp-challenge.ts to prevent replay attacks.
  • Custom before and after hooks enforce waitlist validation, rate limiting (10 requests/minute globally, 3 attempts per OTP), and automatic cleanup of used tokens.
  • Optional OIDC support via buildOidcPlugins() allows hybrid authentication alongside the OTP flow.
  • Sessions include device binding through the deviceId field in session.additionalFields to enhance security across multiple clients.

Frequently Asked Questions

How does Thunderbolt prevent brute-force attacks on the OTP endpoint?

The backend authentication system implements two layers of protection. First, a global rate limit configured in backend/src/auth/auth.ts restricts the OTP endpoint to 10 requests per minute per IP address. Second, the emailOTP plugin enforces a per-challenge limit of 3 attempts, returning TOO_MANY_ATTEMPTS after exhausted tries. IP addresses are correctly extracted using getTrustedIpHeaders when running behind proxies like Cloudflare.

What is the purpose of the challenge token in Thunderbolt's authentication flow?

The challenge token serves as a cryptographic binding between the OTP request and the specific sign-in attempt. Stored in backend/src/dal/otp-challenge.ts via getOrCreateOtpChallenge, this token must be passed in the x-challenge-token header (defined in backend/src/auth/otp-constants.ts) during OTP verification. The before hook validates this token using validateOtpChallenge to prevent replay attacks across different devices or sessions.

Can Thunderbolt authenticate users through corporate identity providers?

Yes, when settings.authMode is set to 'oidc', the createAuth factory invokes buildOidcPlugins() to add OpenID Connect support. This returns a genericOAuth plugin configured with issuer URLs, client credentials, and scopes, redirecting to ${settings.betterAuthUrl}/v1/api/auth/oauth2/callback/oidc. This allows the backend authentication system to support both password-less OTP and enterprise OIDC simultaneously.

How does the waitlist integration gate access to Thunderbolt?

The before hook in backend/src/auth/auth.ts enforces waitlist validation before allowing OTP generation. For new users, the system checks isAutoApprovedDomain in backend/src/waitlist/utils.ts to bypass manual approval for specific domains. Unapproved users are added via createWaitlistEntry and receive waitlist notification emails instead of OTPs, effectively gating access until manual or automatic approval is granted.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →