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

> Configure Open SEO for local development by copying .env.example to .env.local and adding your Cloudflare credentials, database URLs, and API keys.

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

---

**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](https://github.com/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`](https://github.com/every-app/open-seo/blob/main/src/server/lib/runtime-env.ts): Variable Access Layer

All environment variable access flows through **[`src/server/lib/runtime-env.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/runtime-env.ts)**. This module provides type-safe reading with fallback handling:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/scripts/selfhost-preflight.ts), [`cli-auth.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/drizzle.config.ts), migration scripts | D1 database identifier for edge deployment |
| `POSTGRES_DATABASE_URL` | [`drizzle-pg.config.ts`](https://github.com/every-app/open-seo/blob/main/drizzle-pg.config.ts) | PostgreSQL connection string for local/self-hosted instances |

### Authentication Variables

| Variable | Used In | Purpose |
|----------|---------|---------|
| `BETTER_AUTH_URL` | [`cli-auth.ts`](https://github.com/every-app/open-seo/blob/main/cli-auth.ts), auth middleware | URL of the Better Auth service |
| `BETTER_AUTH_SECRET` | [`cli-auth.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/vite.config.ts), client bundle | Exposed to client via `import.meta.env` |
| `PORT` | [`vite.config.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/bad-seo-audit.ts) | Debug logging depth for audit scripts |

## Step-by-Step Setup Instructions

### 1. Copy the Environment Template

```bash
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](https://dash.cloudflare.com):

- **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`:

```bash
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):

```bash

# 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`:

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

```

### 4. Configure Application URLs

```bash
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

```bash
OPENSEO_TELEMETRY_DISABLED=true
DO_NOT_TRACK=true

```

The [`scripts/selfhost-preflight.ts`](https://github.com/every-app/open-seo/blob/main/scripts/selfhost-preflight.ts) script checks these values before initializing analytics:

```typescript
// 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

```bash

# Optional: DataForSEO integration

DATAFORSEO_API_KEY=your-dataforseo-api-key

# Optional: Autumn feature flags

AUTUMN_SECRET_KEY=your-autumn-secret

```

### 7. Start Development Server

```bash
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:

```bash
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](https://vitejs.dev/guide/env-and-mode.html#env-files):

```

.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

```typescript
// 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

```typescript
// 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

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/scripts/selfhost-preflight.ts) validation will catch authentication failures early in the startup process.