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
createServerFnfrom@tanstack/react-start, located insrc/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 usingWorkflowEntrypointandpgStep. - Database Layer: Drizzle ORM definitions in
src/db/schema.tswith 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.
-
Add the table definition in
src/db/schema.tsusing Drizzle's type‑safe column definitions. -
Generate the migration by running
npm run migrateto update the production database schema. -
Create a repository in
src/server/features/<feature>/repositories/following the pattern ofAuditRepositoryinsrc/server/features/audit/repositories/AuditRepository.ts. Repository classes abstract query logic and provide typed methods forinsert,update, andfindoperations.
Updating the Client-Side UI
To consume your new server function from the frontend:
-
Create a page component under
src/app/pages/using TanStack Router for routing. -
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:
src/serverFunctions/keywords.ts: Demonstrates complete server function implementation with validation, middleware, and service delegation.src/server/workflows/SiteAuditWorkflow.ts: Shows durable workflow structure withWorkflowEntrypointandpgStepusage.src/server/workflows/RankCheckWorkflow.ts: Complex example featuring step‑wise retries, credit checks, and workflow finalization.src/db/schema.ts: Central Drizzle schema definitions for all database tables.src/server/features/keywords/services/KeywordResearchService.ts: Business logic service pattern used across multiple endpoints.src/server/lib/posthog.ts: Centralized telemetry implementation for tracking workflow events.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
createServerFnwith Zod validation and middleware insrc/serverFunctions/. - Implement business logic in service classes under
src/server/features/<feature>/services/. - Orchestrate background jobs by extending
WorkflowEntrypointinsrc/server/workflows/and usingpgStepfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →