# How to Configure Cloudflare Access for Self-Hosted OpenSEO: Complete Setup Guide

> Secure your self-hosted OpenSEO with Cloudflare Access. Follow this guide to set environment variables, deploy the Worker, and protect your application.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-09-09

---

**Configure Cloudflare Access for self-hosted OpenSEO by setting the `TEAM_DOMAIN` and `POLICY_AUD` environment variables and deploying the Worker—the middleware in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) automatically verifies the `cf-access-jwt-assertion` header against your Zero Trust policies.**

When you self-host OpenSEO from the `every-app/open-seo` repository on Cloudflare Workers, the platform protects your deployment using **Cloudflare Access** (Zero Trust) to prevent unauthorized API usage. This guide explains how to configure the required environment variables, understand the JWT verification flow, and deploy your secure instance.

## How Cloudflare Access Protects OpenSEO

OpenSEO runs as a Cloudflare Worker, and in self-hosted mode, all incoming requests pass through an Access gate before reaching your application logic. Cloudflare injects a `cf-access-jwt-assertion` header into every authenticated request, which the Worker middleware validates against your team's public keys.

The core verification logic resides in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts). When a request arrives, the `resolveCloudflareAccessContext` function extracts this JWT, fetches the JSON Web Key (JWK) set from `${TEAM_DOMAIN}/cdn-cgi/access/certs`, and validates the token's issuer (`TEAM_DOMAIN`) and audience (`POLICY_AUD`). Upon successful verification, the middleware extracts the user's `sub` (user ID) and `email`, creating a secure workspace context for the request lifecycle.

## Required Environment Variables

Before deploying, you must configure two critical variables that enable Access verification:

- **`TEAM_DOMAIN`**: Your Cloudflare Access team URL (e.g., `https://myteam.cloudflareaccess.com`). The validator in [`src/shared/selfhost-checks.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/selfhost-checks.ts) enforces HTTPS protocol via the `validateTeamDomain` function.
- **`POLICY_AUD`**: The audience tag from your Zero Trust Access application. This identifies the specific Access policy protecting the Worker.

If either variable is missing or malformed, `resolveCloudflareAccessContext` throws an `AppError` with code `AUTH_CONFIG_MISSING`, directing you to [`docs/SELF_HOSTING_CLOUDFLARE.md`](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE.md).

## Step-by-Step Configuration

Follow these steps to configure Cloudflare Access for your self-hosted OpenSEO instance.

### 1. Prepare Your Environment File

Copy the example configuration and add your Cloudflare Access credentials:

```bash
cp .env.selfhost.example .env.selfhost

```

Edit `.env.selfhost` to include:

```dotenv

# Required API keys

DATAFORSEO_API_KEY=your_api_key_here
ACCESS_ALLOWED_EMAILS=admin@yourdomain.com

# Cloudflare Access configuration

TEAM_DOMAIN=https://your-team.cloudflareaccess.com
POLICY_AUD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

```

The `validateTeamDomain` function strictly requires a full HTTPS URL. If you omit the protocol, the validator returns a specific error message prompting you to add the `https://` prefix.

### 2. Authenticate with Cloudflare

Log in to Cloudflare using the Alchemy CLI with the appropriate scopes:

```bash
pnpm alchemy login

```

When prompted, grant the `access:write` scope. If you previously authenticated without this permission, re-run:

```bash
pnpm alchemy login --configure

```

Then bootstrap the internal state store:

```bash
pnpm alchemy cloudflare bootstrap

```

This deploys the necessary infrastructure for managing deployment state.

### 3. Deploy the Self-Hosted Stack

Execute the deployment command:

```bash
pnpm deploy:selfhost --yes

```

This script provisions D1, KV, and R2 resources, runs database migrations, and deploys the Workers. If `TEAM_DOMAIN` and `POLICY_AUD` are present, the script skips automatic Access application creation, assuming you manage the policy manually in the Zero Trust dashboard.

### 4. Verify the Integration

After deployment, visit the Worker URL printed in the terminal. You should be redirected to the Cloudflare Access login screen. After authentication, OpenSEO loads normally.

For troubleshooting, query the health endpoint:

```bash
curl https://<worker-hostname>/api/health

```

This reports runtime configuration checks and database connectivity status.

### 5. (Optional) Manual Access Policy Management

If you prefer direct control over your Zero Trust policies, create the Access application manually in the Cloudflare dashboard. Add the emails specified in `ACCESS_ALLOWED_EMAILS` to the allow list. During redeployment, the script detects your manual configuration via the presence of `TEAM_DOMAIN` and `POLICY_AUD` and preserves your policy settings.

## Understanding the JWT Verification Code

The middleware implementation demonstrates precise security controls. Here is the critical verification path from [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts):

```ts
export async function resolveCloudflareAccessContext(
  headers: Headers,
): Promise<EnsuredUserContext> {
  const teamDomain = env.TEAM_DOMAIN
    ? getValidatedTeamDomain(env.TEAM_DOMAIN)
    : null;
  const policyAud = env.POLICY_AUD?.trim() || null;

  if (!teamDomain || !policyAud) {
    throw new AppError(
      "AUTH_CONFIG_MISSING",
      `Missing Cloudflare Access configuration: set ${missing} on the deployment. See docs/SELF_HOSTING_CLOUDFLARE.md.`,
    );
  }

  const token = headers.get("cf-access-jwt-assertion");
  // ... JWT verification using jose library ...
}

```

The `getValidatedTeamDomain` helper calls `validateTeamDomain` from [`src/shared/selfhost-checks.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/selfhost-checks.ts), which performs strict URL parsing:

```ts
export function validateTeamDomain(value: string): TeamDomainResult {
  const normalized = value.trim().replace(/\/+$/, "");
  try {
    const parsed = new URL(normalized);
    if (parsed.protocol !== "https:") {
      throw new Error("TEAM_DOMAIN must use https");
    }
    return { ok: true, origin: parsed.origin };
  } catch {
    return {
      ok: false,
      message:
        "TEAM_DOMAIN must be a full https URL like https://your-team.cloudflareaccess.com" +
        (normalized && !normalized.includes("://")
          ? ` — add the https:// prefix to "${normalized}"`
          : ""),
    };
  }
}

```

This validation ensures your team domain uses HTTPS and formats correctly before any JWT verification occurs.

## Summary

- **Cloudflare Access** protects self-hosted OpenSEO by verifying JWTs injected by Cloudflare's edge network.
- Configure `TEAM_DOMAIN` and `POLICY_AUD` in `.env.selfhost` to enable authentication; the `validateTeamDomain` function in [`src/shared/selfhost-checks.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/selfhost-checks.ts) enforces strict HTTPS validation.
- The middleware in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) handles `cf-access-jwt-assertion` verification and extracts user contexts including `sub` and `email`.
- Use `pnpm deploy:selfhost --yes` to provision infrastructure; the script respects manually configured Access policies when environment variables are pre-set.
- Reference [`docs/SELF_HOSTING_CLOUDFLARE.md`](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE.md) in the repository for additional deployment scenarios, and note that [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) uses the Access context with `authMode: "cloudflare_access"` to route authenticated requests.

## Frequently Asked Questions

### What happens if I don't set TEAM_DOMAIN and POLICY_AUD?

If these variables are missing, the `resolveCloudflareAccessContext` function throws an `AppError` with code `AUTH_CONFIG_MISSING`. The deployment will fail to authenticate requests, and the error message directs you to the self-hosting documentation to complete the configuration.

### Can I use Cloudflare Access without the automatic deployment script?

Yes. If you manually create the Access application in the Cloudflare Zero Trust dashboard, simply provide the `TEAM_DOMAIN` and `POLICY_AUD` values in your environment file. The `pnpm deploy:selfhost` command detects these variables and skips Access application creation, preserving your manual policy configurations including allowed email lists.

### How does OpenSEO validate the Cloudflare Access JWT?

The Worker fetches the JWK set from `${TEAM_DOMAIN}/cdn-cgi/access/certs` and verifies the `cf-access-jwt-assertion` header using the `jose` library. It validates the issuer matches your `TEAM_DOMAIN` and the audience matches your `POLICY_AUD`. This logic is implemented in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts).

### Where can I find the POLICY_AUD value for my Access application?

The `POLICY_AUD` is the audience tag displayed in your Cloudflare Zero Trust dashboard under Access > Applications. Select your application and copy the Audience Tag value. This unique identifier ensures the Worker only accepts tokens generated for your specific Access policy.