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

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 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. This function instantiates the Better Auth server with environment-specific configurations and exports a singleton used throughout the application.

// 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 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:

// 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:

// 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 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.

// 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). 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.

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 →