# How to Extend Open-SEO Functionality: A Complete Guide to Adding Custom Features

> Extend Open-SEO functionality by creating validated server functions with createServerFn implementing business logic in feature services and orchestrating background jobs

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

---

**Extend Open‑SEO functionality by creating validated server functions with `createServerFn`, implementing business logic in feature services, and orchestrating background jobs through Cloudflare Workflow entrypoints.**

Open‑SEO is a modular TypeScript SEO platform that separates concerns across server functions, workflows, and database layers. Whether you need to add a new API endpoint for the MCP (Modular Cloud‑Platform) AI agents, run background audits, or build new UI components, the codebase provides clear extension points in `every-app/open-seo` that maintain type safety and durability.

## Understanding the Core Architecture

Before extending the system, you must understand the dependency flow that keeps the codebase maintainable. The architecture separates HTTP endpoints, business logic, and data persistence into distinct layers.

The typical request flow follows this path:

`UI Component → Server Function → Feature Service → Repository → Database`

For background processing, the flow becomes:

`Workflow Entrypoint → Service → External API → Repository → Database`

### Key Architectural Layers

- **Server Functions**: HTTP endpoints built with `createServerFn` from `@tanstack/react-start`, located in `src/serverFunctions/`. They handle validation via Zod schemas and middleware for authentication.
- **Feature Services**: Stateless classes in `src/server/features/<feature>/services/` that encapsulate business logic and external API calls (e.g., DataForSEO).
- **Workflows**: Durable Cloudflare Workers in `src/server/workflows/` that manage long‑running, retry‑able background jobs using `WorkflowEntrypoint` and `pgStep`.
- **Database Layer**: Drizzle ORM definitions in [`src/db/schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/schema.ts) with repository classes abstracting query logic.
- **MCP API**: Server functions are automatically exposed to AI agents (Claude, OpenClaw, etc.) via the MCP layer, with skill definitions in `web/content/docs/skills/`.

## Adding a New Server Function

To expose new functionality to the frontend or AI agents, you must create a validated server function that follows the established middleware patterns.

### Step 1: Define a Zod Schema

Create a validation schema in `src/types/schemas/` to ensure type safety across the full stack:

```typescript
// src/types/schemas/sitePerformance.ts
import { z } from "zod";

export const sitePerformanceSchema = z.object({
  projectId: z.string(),
  url: z.string().url(),
});

```

### Step 2: Implement the Endpoint

Add your function under `src/serverFunctions/`, following the pattern established in [`src/serverFunctions/keywords.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/keywords.ts):

```typescript
// src/serverFunctions/sitePerformance.ts
import { createServerFn } from "@tanstack/react-start";
import { sitePerformanceSchema } from "@/types/schemas/sitePerformance";
import { requireProjectContext } from "@/serverFunctions/middleware";
import { SitePerformanceService } from "@/server/features/site-performance/services/SitePerformanceService";

export const generateSitePerformance = createServerFn({ method: "POST" })
  .middleware(requireProjectContext)
  .validator(sitePerformanceSchema)
  .handler(async ({ data, context }) => {
    return SitePerformanceService.runReport({
      url: data.url,
      projectId: context.projectId,
    });
  });

```

This implementation uses `requireProjectContext` middleware to enforce authentication and `createServerFn` to handle the RPC layer automatically.

### Step 3: Expose via MCP

Because the file uses `createServerFn`, it becomes automatically available at `/api/sitePerformance` and via the MCP endpoint at `/mcp`. Update the agent skill documentation in `web/content/docs/skills/` to allow AI agents to discover and use the new capability.

## Creating Background Workflows

For long‑running tasks like sitemap crawling or batch rank checking, you must implement a durable workflow rather than a synchronous server function.

### Step 1: Create the Service Class

Implement the business logic in a service class that the workflow will call:

```typescript
// src/server/features/sitemap/services/SitemapCrawlService.ts
export class SitemapCrawlService {
  static async crawl({ url, projectId }: { url: string; projectId: string }) {
    // Fetch sitemap, parse URLs, store metrics via repository
    return { pagesCrawled: 0, errors: [] };
  }
}

```

### Step 2: Implement the Workflow Entrypoint

Create a workflow class extending `WorkflowEntrypoint`, following the pattern in [`src/server/workflows/SiteAuditWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/SiteAuditWorkflow.ts):

```typescript
// src/server/workflows/SitemapCrawlWorkflow.ts
import {
  WorkflowEntrypoint,
  type WorkflowEvent,
  type WorkflowStep,
} from "cloudflare:workers";
import { withPgClient } from "@/db";
import { pgStep } from "@/server/workflows/pgStep";
import { SitemapCrawlService } from "@/src/server/features/sitemap/services/SitemapCrawlService";

interface CrawlParams {
  projectId: string;
  sitemapUrl: string;
}

export class SitemapCrawlWorkflow extends WorkflowEntrypoint<Env, CrawlParams> {
  async run(event: WorkflowEvent<CrawlParams>, step: WorkflowStep) {
    return withPgClient(() => this.runScoped(event, step));
  }

  private async runScoped(event: WorkflowEvent<CrawlParams>, step: WorkflowStep) {
    const { projectId, sitemapUrl } = event.payload;

    await pgStep(step, "crawl-sitemap", undefined, async () => {
      await SitemapCrawlService.crawl({ url: sitemapUrl, projectId });
    });
  }
}

```

The `pgStep` helper ensures database operations are transactional and retry‑safe, while `withPgClient` manages Postgres connections durably across workflow steps.

### Step 3: Register and Trigger

Register the workflow in the Cloudflare router (typically in [`src/routeTree.gen.ts`](https://github.com/every-app/open-seo/blob/main/src/routeTree.gen.ts)):

```typescript
// src/routeTree.gen.ts (excerpt)
export const routes = {
  "/workflow/sitemap-crawl": SitemapCrawlWorkflow,
};

```

Trigger the workflow from any server function or UI component:

```typescript
await fetch("/api/run-workflow", {
  method: "POST",
  body: JSON.stringify({
    workflow: "sitemap-crawl",
    payload: { projectId: "proj_123", sitemapUrl: "https://example.com/sitemap.xml" },
  }),
});

```

## Extending the Database Layer

When adding features that require new data persistence, you must extend the Drizzle schema and create a repository class.

1. **Add the table definition** in [`src/db/schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/schema.ts) using Drizzle's type‑safe column definitions.

2. **Generate the migration** by running `npm run migrate` to update the production database schema.

3. **Create a repository** in `src/server/features/<feature>/repositories/` following the pattern of `AuditRepository` in [`src/server/features/audit/repositories/AuditRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/repositories/AuditRepository.ts). Repository classes abstract query logic and provide typed methods for `insert`, `update`, and `find` operations.

## Updating the Client-Side UI

To consume your new server function from the frontend:

1. **Create a page component** under `src/app/pages/` using TanStack Router for routing.

2. **Use TanStack Query** to call the MCP endpoint:

```typescript
// src/app/pages/sitePerformance.tsx
import { useMutation } from "@tanstack/react-query";
import { generateSitePerformance } from "@/serverFunctions/sitePerformance";

export default function SitePerformance() {
  const mutation = useMutation({
    mutationFn: (url: string) => 
      generateSitePerformance({ url, projectId: "your-project-id" }),
  });

  const handleSubmit = (url: string) => {
    mutation.mutate(url);
  };

  return (
    <div>
      <form onSubmit={(e) => handleSubmit(e.target.url.value)}>
        <input name="url" placeholder="https://example.com" />
        <button type="submit">Run Report</button>
      </form>
      {mutation.isPending && <p>Processing...</p>}
      {mutation.data && <pre>{JSON.stringify(mutation.data, null, 2)}</pre>}
    </div>
  );
}

```

This pattern mirrors existing pages like [`src/app/pages/keywordResearch.tsx`](https://github.com/every-app/open-seo/blob/main/src/app/pages/keywordResearch.tsx), ensuring consistency in data fetching and error handling.

## Reference Implementation Files

When extending Open‑SEO, study these key source files to understand the implementation patterns:

- **[`src/serverFunctions/keywords.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/keywords.ts)**: Demonstrates complete server function implementation with validation, middleware, and service delegation.
- **[`src/server/workflows/SiteAuditWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/SiteAuditWorkflow.ts)**: Shows durable workflow structure with `WorkflowEntrypoint` and `pgStep` usage.
- **[`src/server/workflows/RankCheckWorkflow.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/RankCheckWorkflow.ts)**: Complex example featuring step‑wise retries, credit checks, and workflow finalization.
- **[`src/db/schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/schema.ts)**: Central Drizzle schema definitions for all database tables.
- **[`src/server/features/keywords/services/KeywordResearchService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/keywords/services/KeywordResearchService.ts)**: Business logic service pattern used across multiple endpoints.
- **[`src/server/lib/posthog.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/posthog.ts)**: Centralized telemetry implementation for tracking workflow events.
- **[`src/server/lib/runtime-env.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/runtime-env.ts)**: Helper for detecting hosted mode, affecting credit check logic.

## Summary

To extend Open‑SEO functionality effectively:

- **Create server functions** using `createServerFn` with Zod validation and middleware in `src/serverFunctions/`.
- **Implement business logic** in service classes under `src/server/features/<feature>/services/`.
- **Orchestrate background jobs** by extending `WorkflowEntrypoint` in `src/server/workflows/` and using `pgStep` for transactional durability.
- **Persist data** by updating [`src/db/schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/schema.ts), running migrations, and creating repository classes.
- **Expose to AI agents** automatically through the MCP layer by adding server functions, then documenting in `web/content/docs/skills/`.
- **Build frontend components** in `src/app/pages/` using TanStack Query to consume your new endpoints.

Following these patterns ensures your extensions leverage built‑in authentication, telemetry, credit checking, and type safety.

## Frequently Asked Questions

### How do I expose a new function to AI agents via the MCP?

**Create the function using `createServerFn` in `src/serverFunctions/` and it automatically becomes available via the MCP endpoint at `/mcp`.** The MCP layer introspects server functions automatically. After creation, update the documentation in `web/content/docs/skills/` to describe the function's purpose and parameters so agents like Claude can discover and invoke it correctly.

### What is the difference between a server function and a workflow?

**Use server functions for synchronous HTTP requests and workflows for asynchronous, durable background processing.** Server functions in `src/serverFunctions/` execute immediately and return results to the client, while workflows in `src/server/workflows/` run as Cloudflare Workers that can survive crashes, retry failed steps, and process long‑running tasks like full site audits or rank checking across thousands of keywords.

### How do I add database persistence for a new feature?

**Define tables in [`src/db/schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/schema.ts), generate migrations with `npm run migrate`, and create a repository class.** The repository pattern used throughout Open‑SEO abstracts Drizzle queries into testable classes (e.g., `AuditRepository`). Place new repositories in `src/server/features/<feature>/repositories/` to maintain clean separation between business logic and data access.

### Can I use external APIs like DataForSEO in my extension?

**Yes, call external APIs from within your feature service classes.** The codebase centralizes external API interactions in services (see [`KeywordResearchService.ts`](https://github.com/every-app/open-seo/blob/main/KeywordResearchService.ts) for reference). Services handle authentication, rate limiting, and error handling, keeping your server functions and workflows clean and focused on orchestration rather than implementation details.