How to Set Up Environment Variables for Local Development of Open SEO

Copy .env.example to .env.local and fill in your Cloudflare credentials, database URLs, and API keys to configure Open SEO for local development.

Open SEO, the open-source SEO platform by every-app, uses a conventional dotenv approach for environment configuration. The project provides a comprehensive template and a centralized runtime helper that validates and loads variables with sensible defaults. This guide walks through the exact variables you need, where they're used in the codebase, and how to configure them properly.

Core Configuration Files

.env.example: The Source of Truth

The repository includes .env.example at the root level containing all required variables with placeholder values. This file serves as the definitive reference for local development setup.

src/server/lib/runtime-env.ts: Variable Access Layer

All environment variable access flows through src/server/lib/runtime-env.ts. This module provides type-safe reading with fallback handling:

// src/server/lib/runtime-env.ts
export function getEnv(name: string): string | undefined {
  return typeof process !== "undefined" ? process.env?.[name] : undefined;
}

export function requireEnv(name: string): string {
  const value = getEnv(name);
  if (!value) throw new Error(`Missing required environment variable: ${name}`);
  return value;
}

The getEnv() function is imported throughout the codebase—in scripts/selfhost-preflight.ts, cli-auth.ts, migration scripts, and configuration files—to ensure consistent access patterns.

Required Environment Variables

Cloudflare Platform Variables

Variable Used In Purpose
CLOUDFLARE_ACCOUNT_ID D1 migrations, Workers deployment Your Cloudflare account identifier
CLOUDFLARE_API_TOKEN D1 migrations, Workers deployment API token with Cloudflare Workers Admin and D1 Edit permissions
CLOUDFLARE_D1_DATABASE_ID drizzle.config.ts, migration scripts D1 database identifier for edge deployment
POSTGRES_DATABASE_URL drizzle-pg.config.ts PostgreSQL connection string for local/self-hosted instances

Authentication Variables

Variable Used In Purpose
BETTER_AUTH_URL cli-auth.ts, auth middleware URL of the Better Auth service
BETTER_AUTH_SECRET cli-auth.ts JWT signing secret (auto-generated if omitted)

Application Variables

Variable Used In Purpose
SITE_URL Server-side rendering, SEO metadata Canonical site URL for link generation
VITE_SITE_URL vite.config.ts, client bundle Exposed to client via import.meta.env
PORT vite.config.ts, dev server Development server port (default: 3000)
NODE_ENV Throughout codebase Runtime environment detection

External Service Variables

Variable Used In Purpose
DATAFORSEO_API_KEY SEO data fetching services API key for DataForSEO integration
AUTUMN_SECRET_KEY Feature flag system Secret for Autumn feature management

Telemetry and Privacy Variables

Variable Used In Purpose
OPENSEO_TELEMETRY_DISABLED scripts/selfhost-preflight.ts Set to true to disable analytics collection
DO_NOT_TRACK Telemetry utilities General DNT flag respected by tracking code

Testing Variables

Variable Used In Purpose
PLAYWRIGHT_CHANNEL playwright.config.ts Browser channel for e2e tests (default: chrome)
DOMAIN_FILTER_CPU_THROTTLE E2E domain overview tests CPU throttling for performance testing
DEBUG_DEPTH bad-seo-audit.ts Debug logging depth for audit scripts

Step-by-Step Setup Instructions

1. Copy the Environment Template

cd open-seo
cp .env.example .env.local

The .env.local file is gitignored by default, preventing credential leaks.

2. Configure Cloudflare Credentials

Obtain from the Cloudflare dashboard:

  • Account ID: Profile → API Tokens → Account ID
  • API Token: Create with Edit permissions for D1 and Cloudflare Workers
  • D1 Database ID: Workers & Pages → D1 → Your database → Settings

Add to .env.local:

CLOUDFLARE_ACCOUNT_ID=1a2b3c4d5e6f7g8h9i0j
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token-here
CLOUDFLARE_D1_DATABASE_ID=00000000-0000-0000-0000-000000000000

3. Set Up Database Connection

For local PostgreSQL (Docker recommended):


# Start PostgreSQL container

docker run -d \
  --name open-seo-postgres \
  -e POSTGRES_USER=openseo \
  -e POSTGRES_PASSWORD=devpassword \
  -e POSTGRES_DB=openseo \
  -p 5432:5432 \
  postgres:16

Add connection string to .env.local:

POSTGRES_DATABASE_URL=postgresql://openseo:devpassword@localhost:5432/openseo

4. Configure Application URLs

SITE_URL=http://localhost:3000
VITE_SITE_URL=http://localhost:3000
PORT=3000

The VITE_ prefix exposes the variable to the client bundle via Vite's import.meta.env system.

5. Optional: Disable Telemetry

OPENSEO_TELEMETRY_DISABLED=true
DO_NOT_TRACK=true

The scripts/selfhost-preflight.ts script checks these values before initializing analytics:

// scripts/selfhost-preflight.ts
import { getEnv } from "../src/server/lib/runtime-env";

function isTelemetryOptOutValue(value: string | undefined): boolean {
  return value === "true" || value === "1" || value === "yes";
}

const telemetryDisabled = getEnv("OPENSEO_TELEMETRY_DISABLED");
const doNotTrack = getEnv("DO_NOT_TRACK");

if (isTelemetryOptOutValue(telemetryDisabled) || isTelemetryOptOutValue(doNotTrack)) {
  console.log("Telemetry disabled by environment configuration");
  // Skip analytics initialization
}

6. Configure External Services


# Optional: DataForSEO integration

DATAFORSEO_API_KEY=your-dataforseo-api-key

# Optional: Autumn feature flags

AUTUMN_SECRET_KEY=your-autumn-secret

7. Start Development Server

npm install
npm run dev

Vite loads .env.local automatically and exposes VITE_-prefixed variables to the client.

Validating Your Configuration

Run the self-host preflight check to verify required variables:

npx tsx scripts/selfhost-preflight.ts

This script validates:

  • Database connectivity (POSTGRES_DATABASE_URL)
  • Cloudflare API token permissions
  • Required authentication secrets

Common Configuration Patterns

Using D1 Locally vs. PostgreSQL

Deployment Primary Database Configuration
Local development PostgreSQL POSTGRES_DATABASE_URL set, D1 variables optional
Cloudflare preview/production D1 CLOUDFLARE_* variables required, Postgres optional
Self-hosted PostgreSQL All CLOUDFLARE_* variables optional

Environment-Specific Files

Open SEO follows Vite's env file priority:


.env                # loaded in all cases

.env.local          # loaded in all cases, ignored by git

.env.[mode]         # loaded for specific mode (dev, staging, production)

.env.[mode].local   # loaded for specific mode, ignored by git

For local development, .env.local is the recommended approach.

Code Examples

Accessing Variables in Server Code

// src/server/db/drizzle-pg.ts
import { getEnv, requireEnv } from "../lib/runtime-env";

const databaseUrl = requireEnv("POSTGRES_DATABASE_URL");
// Throws if missing, ensuring fail-fast behavior

Accessing Variables in Vite Configuration

// vite.config.ts
import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  
  return {
    server: {
      port: parseInt(env.PORT || "3000"),
    },
    define: {
      __SITE_URL__: JSON.stringify(env.VITE_SITE_URL),
    },
  };
});

Conditional Feature Initialization

// src/server/lib/telemetry.ts
import { getEnv } from "./runtime-env";

export function initializeTelemetry(): TelemetryClient | null {
  const disabled = getEnv("OPENSEO_TELEMETRY_DISABLED");
  if (disabled === "true") return null;
  
  // Initialize and return telemetry client
}

Summary

  • Copy .env.example to .env.local as your starting point for local development environment variables for Open SEO
  • Cloudflare credentials (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_D1_DATABASE_ID) are required for D1-based deployments
  • PostgreSQL connection (POSTGRES_DATABASE_URL) is preferred for local development
  • VITE_-prefixed variables are exposed to the client bundle; others remain server-only
  • Disable telemetry with OPENSEO_TELEMETRY_DISABLED=true and DO_NOT_TRACK=true
  • All variable access flows through src/server/lib/runtime-env.ts for consistency and type safety

Frequently Asked Questions

What happens if I don't set BETTER_AUTH_SECRET?

Open SEO auto-generates a random secret on first startup if BETTER_AUTH_SECRET is missing. This is suitable for local development but must be explicitly set in production to ensure consistent JWT validation across server restarts.

Can I use D1 for local development instead of PostgreSQL?

Yes, but it requires running against Cloudflare's remote D1 database or using wrangler dev with local persistence. The default npm run dev command uses Vite's dev server, which connects to PostgreSQL via POSTGRES_DATABASE_URL. For pure D1 local development, use wrangler dev with appropriate Cloudflare credentials configured.

Why are there two URL variables (SITE_URL and VITE_SITE_URL)?

SITE_URL is used by server-side code for SSR, API redirects, and email generation. VITE_SITE_URL is exposed to the browser bundle for client-side routing and API calls. Both should point to the same origin but use different variable names due to Vite's security model—only VITE_-prefixed variables reach the client.

How do I rotate my Cloudflare API token without breaking the app?

Update CLOUDFLARE_API_TOKEN in .env.local, then restart the dev server. For production, update the token in your deployment platform's secret management and trigger a redeployment. The scripts/selfhost-preflight.ts validation will catch authentication failures early in the startup process.

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 →