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.exampleto.env.localas 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=trueandDO_NOT_TRACK=true - All variable access flows through
src/server/lib/runtime-env.tsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →