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

// 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:

// 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:

// 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:

// 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):

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

Trigger the workflow from any server function or UI component:

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 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. 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:

// 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, 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:

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, 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, 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 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.

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 →