# How Authentication is Implemented in Open SEO: Better Auth Architecture Explained

> Discover how Open SEO implements robust authentication with Better Auth and Cloudflare Workers. Supports email password social OAuth and Turnstile for secure SaaS and self-hosted apps.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: architecture
- Published: 2026-07-30

---

**Open SEO implements authentication using Better Auth integrated with Cloudflare Workers, supporting both hosted SaaS and self-hosted deployments with email/password flows, social OAuth providers, Turnstile captcha protection, and automatic organization provisioning.**

The `every-app/open-seo` repository demonstrates a production-grade authentication system built on **Better Auth** and optimized for edge deployment. Understanding how authentication is implemented in Open SEO reveals a sophisticated dual-mode architecture that adapts to both managed SaaS environments and customer-controlled infrastructure. The implementation leverages Cloudflare Workers' runtime capabilities while maintaining strict security boundaries between hosted and self-hosted contexts.

## Architecture Overview: Hosted vs. Self-Hosted Modes

Open SEO operates in two distinct authentication modes determined by the `env.AUTH_MODE` environment variable. When running in **hosted mode**, the application configures URLs via `env.BETTER_AUTH_URL` and enables SaaS-specific features like Cloudflare Turnstile captcha and disposable email blocking. In **self-hosted mode**, the system falls back to `http://localhost` configurations and disables hosted-only security hooks.

The mode detection logic resides in [`src/lib/auth-mode.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth-mode.ts) and influences the entire authentication stack, from database adapter selection to email verification requirements.

## Core Authentication Factory

The entry point for all authentication logic is the `createAuth` factory function in [`src/lib/auth.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth.ts). This function instantiates the Better Auth server with environment-specific configurations and exports a singleton used throughout the application.

```typescript
// src/start.ts
import { createAuth } from "@/lib/auth";
export const auth = createAuth();   // Singleton for the entire application

```

Inside `createAuth`, the factory selects the appropriate database adapter using `drizzleAdapter`, connecting to either Postgres (`pgDb`) or Cloudflare's D1 SQLite (`d1Db`) based on the provider returned by `getDatabaseProvider()`.

## Base Configuration and Security Plugins

Shared authentication settings are defined in [`src/lib/auth-config.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth-config.ts) and merged into the main configuration. The base setup includes session cookie handling via `tanstackStartCookies()` and email/password validation rules.

For hosted deployments, the system conditionally injects the **Turnstile captcha** plugin:

```typescript
// src/lib/auth.ts
const turnstileSecretKey = getHostedTurnstileSecretKey(env);

plugins: [
  ...baseAuthConfig.plugins,
  ...(turnstileSecretKey 
    ? [captcha({ 
        provider: "cloudflare-turnstile", 
        secretKey: turnstileSecretKey, 
        endpoints: ["/sign-up/email"] 
      })] 
    : []),
  tanstackStartCookies(),
],

```

This configuration only activates when `getHostedTurnstileSecretKey` detects a valid secret in the environment, ensuring self-hosted instances aren't burdened with captcha requirements.

## User Lifecycle and Database Hooks

Open SEO enforces business logic through Better Auth's database hooks. During user creation, the system validates email domains and synchronizes contact data.

**Disposable email protection** operates exclusively in hosted mode:

```typescript
// src/lib/auth.ts
before: async (user) => {
  if (isHostedAuthMode(env.AUTH_MODE) && isDisposableEmailDomain(user.email)) {
    throw new APIError("BAD_REQUEST", { 
      message: "Please sign up with a non-disposable email address." 
    });
  }
  return { data: user };
},

```

After successful user creation, the hook synchronizes the contact to the Loops email service. The email flows (verification and password reset) integrate with Loops via `sendHostedVerificationEmail` and `sendHostedPasswordResetEmail`, though verification can be bypassed in development using `BYPASS_EMAIL_VERIFICATION`.

Session creation automatically provisions default organizations for hosted users through `getOrCreateDefaultHostedOrganization`, ensuring every authenticated user has an associated organizational context.

## Social Providers and Trusted Origins

OAuth authentication is configured dynamically through `getSocialProviders()`, which returns the list of enabled social identity providers (Google, GitHub, etc.) to Better Auth.

To secure callbacks, `getTrustedOrigins()` restricts valid redirect URLs to the base URL and development origins when `NODE_ENV !== "production"`, preventing hijacking attacks in non-production environments.

## Session Validation Middleware

The `ensureUser` middleware in [`src/middleware/ensureUser.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensureUser.ts) guarantees that server functions execute only with valid authentication context. It extracts the session cookie, validates it via `auth.api.verifySession`, and injects the resulting context.

```typescript
// src/serverFunctions/projects.ts
import { ensureUser } from "@/middleware/ensureUser";

export const getProjects = ensureUser(async (context) => {
  const { auth } = context; // { userId, organizationId, ... }
  
  return db.project.findMany({ 
    where: { organizationId: auth.organizationId } 
  });
});

```

If validation fails, the middleware returns HTTP 401, preventing unauthorized database access.

## Project-Level Authorization

For MCP-backed tools, Open SEO implements additional authorization through `withMcpProjectAuth` (referenced in tools like [`src/server/mcp/tools/whoami.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/whoami.ts)). This helper verifies that the authenticated user belongs to the specific project being accessed, injecting `auth.organizationId` into tool options after confirming the organization-project relationship.

## Summary

- Open SEO uses **Better Auth** with **Cloudflare Workers** for edge-compatible authentication
- **Dual-mode architecture** supports both SaaS (hosted) and customer-managed (self-hosted) deployments via `env.AUTH_MODE`
- **Database adapters** dynamically switch between Postgres and D1 (SQLite) based on the runtime provider
- **Security layers** include Turnstile captcha (hosted only), disposable email blocking, and trusted origin validation
- **Middleware chain** (`ensureUser` → project auth) ensures every request carries verified user and organization context
- **Email integration** with Loops handles verification and password reset flows

## Frequently Asked Questions

### How does Open SEO handle database compatibility across different hosting providers?

Open SEO abstracts database access through the `getDatabaseProvider()` utility, allowing `createAuth` to instantiate either a Postgres adapter (`pgDb`) for traditional deployments or a D1 adapter (`d1Db`) for Cloudflare's edge SQLite. This enables the same authentication code to run on both serverful and serverless edge infrastructure without modification.

### What prevents automated sign-ups in the hosted SaaS version?

In hosted mode, Open SEO implements **Cloudflare Turnstile** captcha verification on the `/sign-up/email` endpoint. The system also blocks disposable email domains through a `before` hook on user creation that checks `isDisposableEmailDomain()` and throws a `BAD_REQUEST` error for temporary email providers.

### Can email verification be skipped during development?

Yes. By setting the `BYPASS_EMAIL_VERIFICATION` environment variable, developers can disable the email verification requirement. However, production hosted deployments require verification emails sent through the Loops service via `sendHostedVerificationEmail()`.

### How is the authenticated user context passed to server functions?

The `ensureUser` middleware extracts the session from incoming requests, validates it against Better Auth's `verifySession` API, and populates `context.auth` with the `userId` and `organizationId`. This pattern ensures type-safe access to authentication data across all server functions without repetitive validation code.