diff --git a/.env.example b/.env.example
index bff168a..26073fa 100644
--- a/.env.example
+++ b/.env.example
@@ -1,31 +1,22 @@
-# Every App / Gateway settings
-VITE_APP_ID=open-seo
-
-# -------------------------------------------------------------
-# # --- Local development --
-# # Run everyapp app setup-local to automatically configure you .env.local if you've set up your gateway in cloudflare
-# # This should be: https://every-app-gateway.
-# VITE_GATEWAY_URL=
-# GATEWAY_URL=
-
-# # See README.md for instructions for how to get this.
+# -----------------------------------------------------------------------------
+# Core settings
+# -----------------------------------------------------------------------------
+# Required for all modes. See README.md for setup instructions.
# DATAFORSEO_API_KEY=
-# # Choose local app port
-# # PORT=3001
-
-# -------------------------------------------------------------
-# --- Docker self-hosting only (local mode) ---
-# See SELF_HOSTING_DOCKER.md
-# Docker compose runs with BYPASS_GATEWAY_LOCAL_ONLY=true.
-# Optional host/container port (defaults to 3001):
+# Optional app port
# PORT=3001
-# Optional in .env because compose already sets it.
-# BYPASS_GATEWAY_LOCAL_ONLY=true
-# Optional override (defaults to open-seo):
-# VITE_APP_ID=open-seo
-# # See README.md for instructions for how to get this.
-# DATAFORSEO_API_KEY=
+# -----------------------------------------------------------------------------
+# Auth mode
+# -----------------------------------------------------------------------------
+# - cloudflare_access: validate Cloudflare Access JWTs (recommended for deploys)
+# - local_noauth: local trusted mode with injected admin user (admin@localhost)
+# - hosted: reserved for future hosted auth flow (not implemented yet)
+#
+# Defaults to cloudflare_access when unset.
+# AUTH_MODE=cloudflare_access
-# Legacy gateway auth keys are not needed in Docker self-host mode.
+# Required when AUTH_MODE=cloudflare_access
+# TEAM_DOMAIN=https://your-team.cloudflareaccess.com
+# POLICY_AUD=your-cloudflare-access-aud-tag
diff --git a/README.md b/README.md
index 2399487..ee08f44 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ OpenSEO is an SEO tool for _the people_. If tools like Semrush or Ahrefs are too

+[](https://deploy.workers.cloudflare.com/?url=https://github.com/every-app/open-seo)
+
## Table of Contents
- [Why Use This](#why-use-this)
@@ -12,7 +14,9 @@ OpenSEO is an SEO tool for _the people_. If tools like Semrush or Ahrefs are too
- [Community](#community)
- [Pricing / Costs (Free + API costs)](#pricing--costs)
- [DataForSEO API Key Setup](#dataforseo-api-key-setup)
-- [Docker Self Hosting](#docker-self-hosting)
+- [Self-hosting](#self-hosting)
+ - [Cloudflare Deployment + Access Setup](#cloudflare-deployment--access-setup)
+ - [Docker Self Hosting](#docker-self-hosting)
- [Local Development](#local-development)
- [Contributing](#contributing)
- [SEO API Cost Reference](#seo-api-cost-reference)
@@ -80,6 +84,73 @@ printf '%s' 'YOUR_LOGIN:YOUR_PASSWORD' | base64
- Docker self-hosting: `.env`
- Local development: `.env.local`
+## Self-hosting
+
+OpenSEO supports two self-hosting paths:
+
+- Cloudflare for hosting on the internet (Recommended).
+- Docker for your homelab or local use.
+
+Use this quick guide:
+
+- Choose Cloudflare when:
+ - You've never used Docker before.
+ - You want a more SaaS like experience.
+ - You want to use it from multiple devices or with teammates.
+ - You want support for more powerful features in the future like sharing public links to reports or site audits rendering your websites javascript.
+- Choose Docker when:
+ - You already have Docker installed and want to get setup most quickly.
+ - You have a homelab setup.
+ - You only want to use OpenSEO locally on one device.
+
+## Cloudflare Deployment + Access Setup
+
+You can use the Deploy button at the top of this README or run with Wrangler directly.
+
+### 1) Deploy the Worker
+
+Clicking this button will open a page to deploy OpenSEO in your Cloudflare account.
+
+[](https://deploy.workers.cloudflare.com/?url=https://github.com/every-app/open-seo)
+
+### 2) Set required environment variables
+
+OpenSEO needs these vars in production:
+
+- `DATAFORSEO_API_KEY`: base64-encoded `login:password` from DataForSEO.
+- `AUTH_MODE=cloudflare_access`
+- `TEAM_DOMAIN`: your Access team domain (for example `https://your-team.cloudflareaccess.com`).
+- `POLICY_AUD`: Access Application Audience tag for your OpenSEO app.
+
+You can set plain vars and secrets with Wrangler:
+
+```sh
+pnpm exec wrangler secret put DATAFORSEO_API_KEY
+pnpm exec wrangler secret put POLICY_AUD
+pnpm exec wrangler secret put TEAM_DOMAIN
+pnpm exec wrangler secret put AUTH_MODE
+```
+
+You can also set these in Cloudflare Dashboard under Worker Settings.
+
+### 3) Configure Cloudflare Access
+
+In Cloudflare Zero Trust:
+
+1. Create an Access application for your OpenSEO route/domain.
+2. Add policy rules for the identities/groups allowed to access OpenSEO.
+3. Ensure requests to your Worker include `cf-access-jwt-assertion`.
+4. Copy values into OpenSEO config:
+ - Access team domain -> `TEAM_DOMAIN`
+ - Access app AUD tag -> `POLICY_AUD`
+
+### 4) Validate setup
+
+- Visit your OpenSEO URL.
+- You should be prompted by Cloudflare Access if not signed in.
+- After sign-in, the app should load normally.
+- If config is missing, OpenSEO shows an in-app setup error that links back to this section.
+
## Docker Self Hosting
Quickstart:
@@ -113,11 +184,11 @@ cp .env.example .env.local
pnpm install
# Initialize local DB schema (required on a fresh machine)
pnpm run db:migrate:local
-# This runs in BYPASS_GATEWAY mode for local use and quick testing.
+# This runs in local_noauth mode for local use and quick testing.
pnpm dev:agents
```
-`pnpm dev` runs on `http://localhost:3001` by default (or `PORT` from `.env.local`).
+`pnpm dev` runs on `http://localhost:3001` by default (or `PORT` from `.env.local`) in `AUTH_MODE=local_noauth`.
`pnpm dev:agents` runs through [portless](https://github.com/vercel-labs/portless) at `http://open-seo.localhost:1355` by default.
@@ -140,6 +211,17 @@ pnpm dev:agents
- `pnpm dev:agents` mirrors output to `.logs/dev-server.log` (gitignored).
- The log file is overwritten on each run.
+### Auth modes
+
+- `AUTH_MODE=cloudflare_access` (default): validates Cloudflare Access JWTs (`cf-access-jwt-assertion`) using `TEAM_DOMAIN` + `POLICY_AUD`.
+- `AUTH_MODE=local_noauth`: local trusted mode, no auth check, injects `admin@localhost`.
+- `AUTH_MODE=hosted`: reserved for upcoming multi-tenant auth flow (not yet implemented).
+
+Local scripts (`pnpm dev` and `pnpm dev:agents`) set `AUTH_MODE=local_noauth` automatically.
+Use `AUTH_MODE=cloudflare_access pnpm dev` when you specifically want to test Access validation locally.
+
+For Cloudflare deployments, ensure Cloudflare Access is enabled on your Worker route/domain and provide `TEAM_DOMAIN` + `POLICY_AUD` in environment variables.
+
### Database Commands
Generate migration:
diff --git a/SELF_HOSTING_DOCKER.md b/SELF_HOSTING_DOCKER.md
index bc0581e..13be8c1 100644
--- a/SELF_HOSTING_DOCKER.md
+++ b/SELF_HOSTING_DOCKER.md
@@ -1,8 +1,8 @@
# Docker Self-Hosting
-This guide runs OpenSEO as a local service without Every App Gateway.
+This guide runs OpenSEO as a local service.
-In this mode, OpenSEO runs with `BYPASS_GATEWAY_LOCAL_ONLY=true`, so authentication and Gateway-managed user accounts are disabled.
+In this mode, OpenSEO runs with `AUTH_MODE=local_noauth`, so request authentication is disabled and a local admin user (`admin@localhost`) is injected automatically.
## Prerequisites
@@ -13,8 +13,8 @@ In this mode, OpenSEO runs with `BYPASS_GATEWAY_LOCAL_ONLY=true`, so authenticat
This stack is local-first and uses dev runtimes to emulate Cloudflare Worker bindings.
- Do not expose these ports directly to the public internet.
-- There is no built-in Gateway auth in this mode.
-- If you expose it beyond localhost, put it behind the same authentication layer you use for your other self-hosted services (or use the [Cloudflare deployment path](./README.md#self-hosting-deploy-on-cloudflare-5-10-minutes)).
+- There is no built-in auth check in this mode.
+- If you expose it beyond localhost, put it behind the same authentication layer you use for your other self-hosted services (or use the [Cloudflare deployment path](./README.md#cloudflare-deployment--access-setup)).
## 1) Configure env values
@@ -33,8 +33,7 @@ Required:
Optional:
- `PORT` (defaults to `3001`)
-- `VITE_APP_ID` (defaults to `open-seo`)
-- `BYPASS_GATEWAY_LOCAL_ONLY=true` (Docker compose already sets this)
+- `AUTH_MODE=local_noauth` (Docker compose already sets this)
## 2) Start OpenSEO
diff --git a/compose.yaml b/compose.yaml
index 1b5231f..ee5031a 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -6,10 +6,7 @@ services:
working_dir: /app
environment:
- PORT=${PORT:-3001}
- - BYPASS_GATEWAY_LOCAL_ONLY=true
- - VITE_APP_ID=${VITE_APP_ID:-open-seo}
- - VITE_GATEWAY_URL=${VITE_GATEWAY_URL:-http://localhost:${PORT:-3001}}
- - GATEWAY_URL=${GATEWAY_URL:-http://localhost:${PORT:-3001}}
+ - AUTH_MODE=local_noauth
- DATAFORSEO_API_KEY=${DATAFORSEO_API_KEY}
- VITE_SHOW_DEVTOOLS=false
command:
diff --git a/every-app.jsonc b/every-app.jsonc
deleted file mode 100644
index d10ab98..0000000
--- a/every-app.jsonc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "appId": "open-seo",
- "displayName": "OpenSEO",
- "description": "Own your SEO. Research keywords and competitors on your terms.",
-}
diff --git a/knip.jsonc b/knip.jsonc
index a63d875..5916d00 100644
--- a/knip.jsonc
+++ b/knip.jsonc
@@ -1,5 +1,5 @@
{
- "ignoreBinaries": ["everyapp"],
+ "ignoreBinaries": [],
"entry": [
// Detect Tanstack Start Routes
"src/router.tsx",
diff --git a/opencode.jsonc b/opencode.jsonc
index 3f15cc0..f79accb 100644
--- a/opencode.jsonc
+++ b/opencode.jsonc
@@ -5,9 +5,5 @@
"type": "local",
"command": ["npx", "-y", "@upstash/context7-mcp"],
},
- "every-app": {
- "type": "local",
- "command": ["npx", "-y", "@every-app/mcp"],
- },
},
}
diff --git a/package.json b/package.json
index 27dbb15..c24badc 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
"sideEffects": false,
"type": "module",
"scripts": {
- "dev": "vite dev",
- "dev:agents": "mkdir -p .logs && BYPASS_GATEWAY_LOCAL_ONLY=true portless run vite dev 2>&1 | tee .logs/dev-server.log",
+ "dev": "AUTH_MODE=local_noauth vite dev",
+ "dev:agents": "mkdir -p .logs && AUTH_MODE=local_noauth portless run vite dev 2>&1 | tee .logs/dev-server.log",
"build": "vite build && tsc --noEmit",
"lint": "oxlint .",
"lint:fix": "oxlint . --fix",
@@ -16,13 +16,27 @@
"format:check": "prettier --check .",
"format:write": "prettier . --write",
"db:generate": "drizzle-kit generate",
- "db:migrate:local": "drizzle-kit migrate",
- "db:migrate:prod": "npx everyapp app remote-d1-shell -- drizzle-kit migrate --config=drizzle-prod.config.ts",
- "db:studio:local": "drizzle-kit studio",
- "db:studio:prod": "npx everyapp app remote-d1-shell -- drizzle-kit studio --config=drizzle-prod.config.ts",
+ "db:migrate:local": "wrangler d1 migrations apply DB --local",
+ "db:migrate:prod": "wrangler d1 migrations apply DB --remote",
"knip": "knip",
"ci": "prettier --check . && knip && tsc --noEmit && oxlint ."
},
+ "cloudflare": {
+ "bindings": {
+ "AUTH_MODE": {
+ "description": "Set to `cloudflare_access` for secured deployments behind Cloudflare Access, or `local_noauth` for trusted local self-host mode only."
+ },
+ "TEAM_DOMAIN": {
+ "description": "Cloudflare Access team domain (for example `https://your-team.cloudflareaccess.com`) used to validate `cf-access-jwt-assertion` JWTs."
+ },
+ "POLICY_AUD": {
+ "description": "Cloudflare Access Application Audience (AUD) tag for this Worker route/domain."
+ },
+ "DATAFORSEO_API_KEY": {
+ "description": "Base64-encoded `login:password` for DataForSEO API access."
+ }
+ }
+ },
"dependencies": {
"@every-app/sdk": "^0.1.13",
"@tanstack/query-core": "^5.90.9",
diff --git a/src/client/components/AuthConfigErrorCard.tsx b/src/client/components/AuthConfigErrorCard.tsx
new file mode 100644
index 0000000..f08989d
--- /dev/null
+++ b/src/client/components/AuthConfigErrorCard.tsx
@@ -0,0 +1,52 @@
+import { ShieldAlert } from "lucide-react";
+
+const README_CLOUDFLARE_ACCESS_URL =
+ "https://github.com/every-app/open-seo#cloudflare-deployment--access-setup";
+
+type AuthConfigErrorCardProps = {
+ message: string;
+ onRetry?: () => void;
+};
+
+export function AuthConfigErrorCard({
+ message,
+ onRetry,
+}: AuthConfigErrorCardProps) {
+ return (
+
+
+
+
+ Cloudflare Access setup required
+
+
+
+ {message}
+
+
+
+ This deployment is missing required Access settings for
+ AUTH_MODE=cloudflare_access. Configure{" "}
+ TEAM_DOMAIN and
+ POLICY_AUD, then retry.
+