# How to Deploy OpenSEO on Cloudflare Workers: A Complete Self-Hosting Guide

> Deploy OpenSEO on Cloudflare Workers easily. This guide shows you how to self-host OpenSEO with one command using Alchemy CLI for a complete setup.

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

---

**Deploy OpenSEO on Cloudflare Workers using Alchemy CLI with one command that provisions D1, KV, R2, Durable Objects, and optional Cloudflare Access protection.**

OpenSEO is an open-source SEO platform that runs as a Cloudflare Workers service. This guide walks you through deploying it on your own Cloudflare account, from initial setup to production operations, using the exact configuration and tooling maintained in the [every-app/open-seo](https://github.com/every-app/open-seo) repository.

## Architecture Overview

OpenSEO's Cloudflare deployment relies on a coordinated stack of edge services. The **entry point** is [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), which inspects incoming requests and routes them to the appropriate handler:

- **MCP server** for tool-based integrations
- **Agent Durable Objects** for stateful chat sessions (`ONBOARDING_CHAT`, `SAM_CHAT`)
- **React-Start application** for the main UI

The deployment configuration lives in `wrangler.jsonc`, which declares bindings for **D1 database** (`DB`), **KV namespaces** (`KV`, `OAUTH_KV`), **R2 bucket** (`R2`), and **Durable Object classes**. It also defines two **Workflow bindings** (`site-audit-workflow`, `rank-check-workflow`) and a cron schedule for background jobs.

## Prerequisites

Before deploying, ensure you have:

- A Cloudflare account with Workers Paid plan (required for Durable Objects and D1)
- **DataForSEO API key** — this is mandatory for core functionality
- Node.js 20+ with `corepack` enabled
- `pnpm` package manager (bootstrapped via corepack)

## Step-by-Step Deployment

### 1. Clone and Install

```bash
git clone https://github.com/every-app/open-seo.git
cd open-seo
corepack enable
pnpm install

```

Fork the repository first if you plan to maintain custom modifications.

### 2. Authenticate with Cloudflare

The **Alchemy** provisioning framework handles infrastructure-as-code deployment:

```bash
pnpm alchemy login                # Grants access:write scope

pnpm alchemy cloudflare bootstrap # Creates Alchemy state-store Worker

```

The bootstrap command creates a dedicated Worker that tracks Alchemy's deployment state.

### 3. Configure Environment Variables

Copy the template and provide your secrets:

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

```

Edit `.env.selfhost` with at minimum these values:

| Variable | Required | Purpose |
|----------|----------|---------|
| `DATAFORSEO_API_KEY` | ✅ | DataForSEO API access for rank tracking and audits |
| `ACCESS_ALLOWED_EMAILS` | ✅ | Comma-separated emails authorized via Cloudflare Access |

Optional variables include:
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` — Google Search Console integration
- `SAM_ENABLED=true` — Enable in-app SAM agents
- `TELEMETRY_DISABLED=1` — Disable analytics collection
- `ACCESS_DOMAIN` — Custom Cloudflare Access application domain

### 4. Deploy with One Command

```bash
pnpm deploy:selfhost --yes

```

This single command performs the complete **OpenSEO Cloudflare Workers deployment**:

- Creates D1 database and applies migrations
- Provisions KV namespaces and R2 bucket
- Deploys the main Worker and auxiliary workflow Workers
- Creates Cloudflare Access application (if `ACCESS_ALLOWED_EMAILS` is set)
- Injects all secrets as Worker environment bindings

### 5. Verify Installation

```bash
open "https://$(grep 'name' wrangler.jsonc | head -1 | sed 's/.*"name": "\([^"]*\)".*/\1/').workers.dev"

```

You should encounter a Cloudflare Access login screen, then the OpenSEO dashboard upon successful authentication.

## Managing Your Deployment

### Updating OpenSEO

Pull latest changes and redeploy:

```bash
git pull
pnpm install
pnpm deploy:selfhost --yes

```

If working from a fork, sync with upstream first:

```bash
git fetch upstream
git merge upstream/main

```

### Adding Team Members

Modify `ACCESS_ALLOWED_EMAILS` in `.env.selfhost` and redeploy:

```bash

# Edit .env.selfhost with additional emails

pnpm deploy:selfhost --yes

```

No manual Cloudflare dashboard changes required.

### Complete Teardown

Remove all provisioned resources:

```bash
pnpm alchemy destroy --env-file .env.selfhost --stage selfhost

```

**Warning:** This permanently deletes the D1 database, KV data, R2 objects, and all Workers.

## Configuration Deep Dive

### `wrangler.jsonc` Bindings

The deployment declares these **Cloudflare resource bindings**:

- **`DB`** — D1 database for relational data (migrations, user accounts, audit history)
- **`KV`** / **`OAUTH_KV`** — Separate KV namespaces for app state and OAuth token storage
- **`R2`** — Object storage for crawl archives and report exports
- **`ONBOARDING_CHAT`** / **`SAM_CHAT`** — Durable Object namespaces for persistent agent conversations
- **Workflow bindings** — `site-audit-workflow` and `rank-check-workflow` for background job orchestration

Cron triggers execute at configured intervals to:
- Refresh rank tracking data
- Reconcile audit queue state
- Expire stale OAuth tokens in `OAUTH_KV`

### Security Model

Cloudflare Access provides **zero-trust authentication** for both web UI and MCP server access. The Access application is automatically configured when `ACCESS_ALLOWED_EMAILS` is present. For production hardening, consult **[`docs/SELF_HOSTING_CLOUDFLARE_OPERATIONS.md`](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE_OPERATIONS.md)** which covers:

- MCP server access via Cloudflare Access service tokens
- Disabling telemetry with `TELEMETRY_DISABLED=1`
- Custom domain configuration

## Troubleshooting Common Issues

| Symptom | Likely Cause | Fix |
|---------|------------|-----|
| `DB binding not found` | Missing D1 migration | Run `pnpm wrangler d1 migrations apply DB --env selfhost` |
| Access login loop | Email not in `ACCESS_ALLOWED_EMAILS` | Add email to `.env.selfhost` and redeploy |
| MCP tools unreachable | Cloudflare Access blocking requests | Configure service token authentication per operations doc |
| Workflow failures | Cron trigger misconfiguration | Verify `wrangler.jsonc` `triggers.crons` array |

## Summary

To **deploy OpenSEO on Cloudflare Workers**:

- Use **`pnpm deploy:selfhost --yes`** for one-command provisioning via Alchemy
- Configure **`DATAFORSEO_API_KEY`** and **`ACCESS_ALLOWED_EMAILS`** in `.env.selfhost`
- Reference **`wrangler.jsonc`** for all resource bindings and cron schedules
- Read **[`docs/SELF_HOSTING_CLOUDFLARE.md`](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE.md)** for detailed walkthroughs
- Maintain **[`docs/SELF_HOSTING_CLOUDFLARE_OPERATIONS.md`](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE_OPERATIONS.md)** for security and monitoring guidance

The entire stack — D1, KV, R2, Durable Objects, Workflows, and Access — deploys automatically without manual Cloudflare dashboard configuration.

## Frequently Asked Questions

### Does OpenSEO require a paid Cloudflare plan?

Yes. Durable Objects and D1 free tiers have strict limits that OpenSEO exceeds during normal operation. The Workers Paid plan ($5/month minimum) is required for production deployments. Alchemy provisions resources within your existing account billing.

### Can I use a custom domain instead of workers.dev?

Absolutely. Set `ACCESS_DOMAIN` in `.env.selfhost` to your custom hostname before deployment. Alchemy configures the Cloudflare Access application and DNS records automatically. If omitted, the deployment uses the default `workers.dev` subdomain derived from the Worker name in `wrangler.jsonc`.

### How do I back up the D1 database?

Standard D1 backup procedures apply. Use `wrangler d1 export` for on-demand dumps, or configure periodic exports via Cloudflare API. The Alchemy state-store Worker does not include automated backup logic — implement external backup workflows for production data protection.

### What happens if my DataForSEO API key expires?

Rank checking and site audits will fail silently in background workflows, with errors logged to the Worker's tail logs. The UI continues functioning for historical data review. Renew the key in DataForSEO dashboard, update `DATAFORSEO_API_KEY` in `.env.selfhost`, and redeploy — no data migration required.