llms.txt
llms.txt

Integrations

5 min read

Integrations are the Farm layer for connecting product services to your app. A payment provider, auth system, email service, job runner, API-key platform, UI registry, or internal company SDK can register everything it owns in one place: route handlers, typed client/server callers, request middleware, React providers, storage schemas, lifecycle hooks, and runtime logs.

Farm treats every integration as a small server plugin. That means an integration can participate in framework startup and shutdown, own HTTP routes, and still expose a compact typed API to the rest of the app.

Register integrations

farm.config.ts
import { defineConfig } from "@farmjs/core";import { stripe } from "@farmjs/integrations/stripe";import { betterAuth } from "@farmjs/integrations/better-auth";export default defineConfig({  integrations: {    billing: stripe({      secretKey: process.env.STRIPE_SECRET_KEY,      webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,    }),    auth: betterAuth({      baseURL: process.env.BETTER_AUTH_URL,      secret: process.env.BETTER_AUTH_SECRET,    }),  },});

The object key is the application namespace. If you register Stripe as billing, the typed caller lives at api.billing. If you register it as stripe, it lives at api.stripe.

Create callers

src/lib/api.ts
import { createIntegrations } from "@farmjs/core/client";import type { AppIntegrations } from "./integrations";export const { api, apiClient } = createIntegrations<AppIntegrations>({  data: {    appName: "farm-dashboard",  },});

apiClient is the browser caller. api is the server caller. Both preserve the same integration namespace so a route like /api/billing/checkout can become api.billing.checkout.post(...), and a single-method endpoint can be called directly when there is only one method.

Integration surface

An integration can contribute any of these pieces. The three HTTP fields are alternatives for different authoring needs; you normally do not define all three.

HTTP field Handles requests? Produces typed callers? Use it when
routes Yes Yes, when entries use integrationRoute.* The integration owns the handlers and a flat array or route factory is easiest to read. This is the recommended default.
endpoints Yes Yes, when entries use endpoint.* The integration owns the handlers, but a nested object makes a large set of routes easier to organize. Farm flattens it into routes.
api No Yes The HTTP handlers already exist elsewhere, or you deliberately need a custom caller tree. It is a caller contract, not a route handler.

For typed routes and endpoints, Farm derives the caller tree from each URL. For example, /api/billing/checkout becomes api.billing.checkout.post(...) after the integration is registered as billing. The object keys inside endpoints only organize the source code; they do not rename the derived caller.

Plain route objects still mount handlers, but they do not carry typed caller metadata. Use integrationRoute.* or the endpoint.* factory when you want Farm to infer request and response types for api and apiClient.

An explicit api field replaces that derived caller tree for the integration. It does not mount an HTTP handler, so every operation in it must point to a route implemented by routes, endpoints, the app, or another server.

The api: field in an integration definition is the caller contract. The api returned by createIntegrations is the server caller built from that contract; apiClient is its browser counterpart. See Choosing routes, endpoints, or api for side-by-side examples.

Other integration fields are independent of that HTTP choice:

Surface What it is for
middleware Integration-owned request behavior for matchers outside a single endpoint.
providers React provider metadata and optional wrapper components.
schema Storage models used by ctx.args.db through the ORM layer.
config Schema-validated config from defaults, env, input, and resolver output.
validate Early checks before the integration starts.
setup Bootstrapping work such as storage checks or webhook registration.
ready Post-start work once the app is ready.
dispose Shutdown cleanup.
log Runtime events for registration, request start, request end, request error, and lifecycle messages.
plugins Extra Farm plugins that ship with the integration.
documentNavigations Route matchers that tell docs/navigation systems where the integration owns pages.

Route ownership

Integration handlers declared through either routes or endpoints use the same web primitives as normal route handlers. The handler receives the Request and a context object with params, parsed input, request metadata, shared request context, integration metadata, and args for storage.

src/integrations/local-demo.ts
import { defineIntegration, integrationRoute } from "@farmjs/core";import { z } from "zod";export const localDemo = defineIntegration({  category: "custom",  type: "local-demo",  instance: {},  routes: [    integrationRoute.post<      "/api/local-demo/messages",      { message: string },      { message: string; count: number },      { count: number }    >("/api/local-demo/messages", {      body: z.object({        message: z.string().min(2),      }),      query: z.object({        count: z.coerce.number().int().positive(),      }),      handler(_request, ctx) {        return Response.json({          message: ctx.input.body?.message,          count: ctx.input.query?.count ?? 1,        });      },    }),  ],});

Farm validates the body and query before route middleware, before hooks, or the handler run. Invalid input returns a 400 response with validation issues.

Caller shape

Typed routes derive their caller tree from their path, so this integration does not need an explicit api field:

src/lib/api.ts
import { createIntegrations } from "@farmjs/core/client";import { localDemo } from "../integrations/local-demo";export const { api, apiClient } = createIntegrations({  localDemo,});
"use client";import { apiClient } from "../lib/api";export function SendMessageButton() {  async function send() {    const result = await apiClient.localDemo.messages.post({      body: {        message: "hello",      },      query: {        count: 2,      },    });    if (result.error) {      throw result.error;    }    console.log(result.data.message);  }  return <button onClick={send}>Send</button>;}
import { api } from "../lib/api";export async function loadMessage() {  const result = await api.localDemo.messages.post({    body: {      message: "server hello",    },  });  return result.data;}

On the server, Farm first tries to dispatch to the registered integration runtime directly. If no runtime is available, it falls back to fetch with forwarded request headers.

Agent runtimes

Agent integrations connect a provider runtime to the Farm lifecycle without replacing that provider's SDK. Farm starts the runtime beside the app in development, owns its same-origin route prefixes, and composes supported production output. The client still uses the provider-native hooks and typed RPC APIs.

import { defineConfig } from "@farmjs/core";import { eve } from "@farmjs/eve";export default defineConfig({  integrations: {    agent: eve(),  },});

agent is a conventional namespace, not a reserved key. Agent runtime routes do not generate api or apiClient callers because streaming and WebSocket protocols are handled by the provider SDK.

Runtime Farm manages Application uses
Eve Eve development process, /eve and workflow routes, Vercel build composition. agent/ files and useEveAgent().
Cloudflare Agents Wrangler development process, /agents WebSocket proxy, combined Worker output and deployment metadata. Agent classes, Durable Object bindings, useAgent(), and callable RPC.

Same-origin routing is not an authorization boundary. Authenticate agent HTTP and WebSocket requests in application middleware or provider routing hooks, and authorize every sensitive tool or callable method on the server.

Shared data

createIntegrations({ data }) adds small per-call metadata to integration requests. It is useful for tenant IDs, locale, analytics context, or feature flags.

export const { apiClient } = createIntegrations<AppIntegrations>({  data: {    tenantId: "tenant_123",    locale: "en",  },});await apiClient.billing.checkout.post(  {    body: {      priceId: "price_123",    },  },  {    data: {      source: "settings-page",    },  },);

The route reads that value from ctx.data. Browser-provided data is client controlled, size limited, and sanitized before it reaches the handler. Validate it before using it for authorization.

Built-in groups

Group Built-ins
Payment Stripe, Autumn, and Polar expose checkout, subscription, portal, webhook, entitlement, and billing snapshot patterns.
Auth Better Auth, Auth.js, Clerk, Auth0, WorkOS, and Supabase expose routes, providers, session helpers, and auth middleware.
Messaging Resend sends transactional mail, previews templates, and receives provider webhooks.
Workflows Trigger.dev and Inngest expose trigger, schedule, batch, status, and cancel APIs.
Agents Eve and Cloudflare Agents run beside Farm in development and compose with supported deployment targets.
API Keys Unkey can create, verify, revoke, update, and delete customer or service keys.
Interface UI registry entries scaffold shadcn-style integration screens when --ui is enabled.
Storage ORM storage gives schema-backed integrations a provider-neutral ctx.args.db.

When to build your own

Create a custom integration when a service needs one or more of these:

  • Routes or webhooks that should be mounted automatically.
  • Typed client/server functions that should not be hand-written in each app.
  • Config validation that should fail during startup.
  • Storage models shared by routes, hooks, and built-in UI.
  • Request middleware that belongs to the service.
  • Setup or teardown work such as webhook registration, queue consumers, or health checks.

For the full authoring interface, see Custom Integrations.