Open-SEO Best Practices: A Complete Guide to Type-Safe SEO Infrastructure

Deploy Open-SEO successfully by following six setup steps—install skills, configure environment, run pre-flight checks, choose Docker or Cloudflare Workers, migrate your database, and leverage typed server functions for end-to-end type safety.

Open-SEO is a full-stack, TypeScript-first SEO platform built on modern primitives: TanStack Server Functions, Drizzle ORM, Zod validation, and Better-Auth. Understanding open-seo best practices ensures you deploy securely, extend confidently, and avoid common configuration pitfalls. This guide distills the repository's architecture into actionable patterns drawn directly from the every-app/open-seo source code.


Setting Up Open-SEO for Production

Install the Skill Set First

Before touching code, install the CLI-driven Open-SEO commands that handle project creation, rank tracking, and more:


# Install the skill set

npx skills add every-app/open-seo

# Or install all available skills

npx skills add every-app/open-seo --skill '*'

This exposes commands like npx open-seo project create that call typed server functions internally. Reference: web/content/docs/skills/setup.md.

Configure Environment Secrets

Copy the template and fill required values:

cp .env.example .env

Critical secrets include:

  • DataForSEO API key – for rank tracking APIs
  • Google OAuth client – for authentication flows

The .env.example file lists all required keys without exposing actual values, following twelve-factor app principles.

Run Pre-Flight Validation

Execute the self-host preflight script before starting services:

pnpm exec tsx scripts/selfhost-preflight.ts

Or rely on docker-entrypoint.sh to run it automatically. The runSelfhostPreflight() function in src/lib/selfhost-preflight.ts verifies:

  • Database connectivity and migration status
  • Presence of required environment variables
  • Reachability of external APIs (DataForSEO, Google OAuth)

This prevents containers from starting in broken states—a critical open-seo best practice for production deployments.


Deployment Targets: Docker vs. Cloudflare Workers

Docker Self-Hosting


# Pull and run the official image

docker compose up -d

The Dockerfile.selfhost includes automatic preflight checks in its entrypoint. Ideal for local development, VMs, orprivate infrastructure.

Cloudflare Workers Edge Deployment


# After configuring wrangler.toml

wrangler publish

Reference configuration lives in badseo/wrangler.jsonc. Workers deployment provides edge-scale with zero-ops infrastructure—no container management required.

Both paths are officially maintained. Choose Docker for simplicity, Workers for automatic global distribution.


Database Layer with Drizzle ORM

Open-SEO uses Drizzle for compile-time-checked SQL across SQLite, D1, and Postgres. Schema definitions in src/db/schema.ts guarantee column-level safety:

// src/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const projects = sqliteTable("project", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" }).defaultNow(),
});

Run migrations before starting the app:

pnpm drizzle-kit generate
pnpm drizzle-kit migrate

The same schema compiles to Postgres via drizzle.config.ts, enabling database portability without code changes.


Typed Server Functions Architecture

Every public endpoint lives under src/serverFunctions/* and follows a consistent pattern. The getProject function in src/serverFunctions/projects.ts demonstrates:

// src/serverFunctions/projects.ts
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/db/client";
import { projects } from "@/db/schema";

export const getProject = async (input: { id: string }) => {
  const schema = z.object({ id: z.string().uuid() });
  const { id } = schema.parse(input);
  return db.select().from(projects).where(eq(projects.id, id));
};

Key characteristics:

  • Zod validation runs before any database call
  • TanStack Server Functions expose the endpoint with perfect client-side type inference
  • Drizzle ORM provides type-safe query building

Call from the front-end with full IntelliSense:

import { createProject } from "@/serverFunctions/projects";

async function handleCreate() {
  const result = await createProject({ name: "My Blog" });
  // result is fully typed based on the Zod schema
  console.log("Created project:", result);
}

Schema-First Validation Patterns

Centralize all contracts in src/types/schemas/*.ts. The rank-tracking schema in src/types/schemas/rank-tracking.ts shows the pattern:

// src/types/schemas/rank-tracking.ts
import { z } from "zod";

export const RankTrackingSchema = z.object({
  projectId: z.string().uuid(),
  keyword: z.string().min(1),
  targetUrl: z.string().url(),
  intervalDays: z.number().int().min(1).max(30),
});

Benefits:


Adding Custom Endpoints

Extend Open-SEO by following the established three-step pattern:

Step 1: Define the schema

// src/types/schemas/custom.ts
import { z } from "zod";

export const CustomInput = z.object({
  foo: z.string(),
  bar: z.number().int(),
});

Step 2: Create the server function

// src/serverFunctions/custom.ts
import { CustomInput } from "@/types/schemas/custom";

export const doSomething = async (input: unknown) => {
  const { foo, bar } = CustomInput.parse(input);
  // business logic
  return { success: true, echo: `${foo}-${bar}` };
};

Step 3: Consume from client

Client code calls doSomething(...) with compile-time type safety and runtime validation guaranteed.


Authentication and Security

Open-SEO uses better-auth with a Drizzle adapter, configured in src/lib/auth.ts:

// src/lib/auth.ts
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { d1Db } from "./db/client";

export const auth = createAuth({
  adapter: drizzleAdapter(d1Db, { /* schema mapping */ }),
  // additional options
});

The src/middleware/ensureUser/* middleware enforces user presence on protected routes, returning 401 for unauthenticated requests. Reuse this pattern for any custom protected endpoints.


Privacy and Telemetry

Disable telemetry for privacy-first deployments:

OPENSEO_TELEMETRY_DISABLED=1

# or

DO_NOT_TRACK=1

Documented in web/content/docs/self-hosting/docker.md, this aligns with modern privacy regulations and user expectations.


Key Files Reference

Bookmark these paths for daily development:


Summary

  • Install skills first with npx skills add every-app/open-seo to enable CLI workflows
  • Validate before starting using scripts/selfhost-preflight.ts to catch configuration errors early
  • Choose Docker or Workers based on your infrastructure needs—both are first-class
  • Run Drizzle migrations to sync schema across SQLite, D1, or Postgres targets
  • Follow the three-layer pattern – Zod schema, server function, client call – for type-safe extensions
  • Disable telemetry with environment variables when privacy compliance requires it

Frequently Asked Questions

What database does Open-SEO support?

Open-SEO supports SQLite, Cloudflare D1, and Postgres through Drizzle ORM. The same schema definitions in src/db/schema.ts compile to all three targets via drizzle.config.ts, enabling seamless database portability without code changes.

How does Open-SEO validate API inputs?

All inputs pass through Zod schemas defined in src/types/schemas/*.ts. Server functions parse and validate payloads before database access, providing both runtime safety and compile-time type inference on the client. This pattern prevents invalid data from reaching your database.

Can I self-host Open-SEO without Docker?

Yes. Deploy to Cloudflare Workers using wrangler publish after configuring wrangler.toml. The Workers path provides edge-scale distribution without container management. Both Docker and Workers deployments run identical pre-flight checks to validate environment readiness.

What happens if required environment variables are missing?

The runSelfhostPreflight() function in src/lib/selfhost-preflight.ts detects missing variables during startup and prevents the server from launching. When using Docker, docker-entrypoint.sh runs these checks automatically, ensuring the container fails fast with a clear error message rather than starting in a broken state.

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 →