# How Thunderbolt's Backend Authentication System Works with Better Auth

> Explore Thunderbolt's password-less backend authentication using Better Auth. Learn how OTP sign-in, PostgreSQL persistence, and waitlist hooks work together for secure access.

- Repository: [Thunderbird/thunderbolt](https://github.com/thunderbird/thunderbolt)
- Tags: internals
- Published: 2026-04-19

---

**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`](https://github.com/thunderbird/thunderbolt/blob/main/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`:

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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:

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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:

```typescript
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

```typescript
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)

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/auth/utils.ts).

### Complete Sign-in with OTP

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/auth/auth.ts) | Core `createAuth` factory, Better Auth configuration, plugins, and hooks |
| [`backend/src/dal/otp-challenge.ts`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/dal/otp-challenge.ts) | Challenge token storage with `getOrCreateOtpChallenge`, `validateOtpChallenge`, and cleanup functions |
| [`backend/src/auth/otp-constants.ts`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/auth/otp-constants.ts) | Constants for OTP expiry durations and the `x-challenge-token` header name |
| [`backend/src/auth/utils.ts`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/auth/utils.ts) | Utility functions including `buildVerifyUrl` for magic links and `getValidatedOrigin` for CORS validation |
| [`backend/src/waitlist/utils.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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.