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:
- API evolution without breaking callers
- Round-trip tests in
src/types/schemas/rank-tracking.test.ts - Single source of truth for validation logic
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:
- Server functions –
src/serverFunctions/projects.ts– Project CRUD API - Database schema –
src/db/schema.ts– Central type definitions - Zod contracts –
src/types/schemas/projects.ts– Payload validation - Auth layer –
src/lib/auth.ts– Better-Auth integration - Self-host checks –
src/lib/selfhost-preflight.ts– Startup validation - Docker config –
Dockerfile.selfhost– Production container - Workers config –
badseo/wrangler.jsonc– Edge deployment
Summary
- Install skills first with
npx skills add every-app/open-seoto enable CLI workflows - Validate before starting using
scripts/selfhost-preflight.tsto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →