Farm.js

Integrations

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

A plugin extends framework behavior; an integration owns a configured service capability, such as its SDK client, lifecycle, models, or providers. They can share route machinery without being the same public contract. Their API routes can be consumed through one shared API setup.

Import the dedicated adapter package

New applications should import each Farm adapter from its dedicated package, such as @farm.js/stripe, @farm.js/clerk, or @farm.js/jobs. These are the same packages installed by farm add integration and used by the generated starter files.

The older @farm.js/integrations/* paths are compatibility re-exports for existing applications. They are not a separate ownership model, and new documentation does not use them.

A Farm adapter function such as stripe(...) registers routes, typed callers, webhooks, and integration lifecycle behavior. It is not the Stripe SDK instance. When the application supplies instance, the adapter uses that exact object instead of constructing another provider client.

Provider adapters support two first-class ownership modes. Pass credentials when the adapter should construct its default SDK client, or pass the vendor SDK object through instance when the application should own that client. These are alternatives; an application normally registers only one Stripe integration for a given billing namespace.

Register integrations

farm.config.ts
import { defineConfig } from "@farm.js/core";
import { stripe } from "@farm.js/stripe";
import { betterAuth as createBetterAuth } from "better-auth";
import { betterAuth } from "@farm.js/better-auth";

const auth = createBetterAuth({
  baseURL: process.env.BETTER_AUTH_URL,
  secret: process.env.BETTER_AUTH_SECRET,
});

export default defineConfig({
  integrations: {
    billing: stripe({
      secretKey: process.env.STRIPE_SECRET_KEY,
      webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
    }),
    auth: betterAuth({
      instance: auth,
    }),
  },
});

The object key is the application namespace. Registering Stripe as billing exposes api.integrations.billing with createApiClients, or api.billing with the integration-only createIntegrations factory. The same distinction applies to apiClient; registering it as stripe changes the billing segment to stripe.

Own the provider instance in application code

When an integration uses an in-process vendor SDK, pass a configured client through instance. Farm uses that exact object and does not create a second client. This lets the application control SDK versions, transports, retries, telemetry, test doubles, and provider-specific options without waiting for the Farm adapter to expose every constructor option.

src/lib/stripe-client.ts
import Stripe from "stripe";

export const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  maxNetworkRetries: 2,
});
src/lib/integrations.ts
import { stripe } from "@farm.js/stripe";
import { stripeClient } from "./stripe-client";

export const billing = stripe({
  instance: stripeClient,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
});

The vendor SDK comes from the vendor package, while the Farm adapter comes from the dedicated Farm package. Keep the provider client in server-only application code and reuse it wherever direct SDK access is needed. The adapter still owns Farm-specific configuration such as webhook routes, product metadata, billing ownership, and typed callers.

Credential-based construction and instance injection are both supported. When instance is omitted, an adapter such as @farm.js/stripe can construct its default SDK client from secretKey or the matching environment variable. When both are supplied, instance wins. The injected object must still implement the SDK methods the Farm adapter calls; a vendor breaking those methods can still require an adapter update.

Integration kindApplication ownership boundary
Stripe, Autumn, Polar, Resend, Clerk, WorkOSinstance: a vendor SDK object constructed in application code.
Auth.js and Better Authinstance: the application-owned auth object. These adapters have no Farm-constructed fallback.
Auth0instance: a compatible application middleware adapter, not the Auth0 SDK. It replaces Farm's built-in route flow.
Unkeyinstance: an application-owned UnkeyClient; createUnkeyClient is an optional convenience constructor.
Supabase SSRinstance: a request-scoped factory that receives Farm's cookie-aware client options. Never share one SSR auth client between requests.
AImodel plus optional AI SDK function overrides. The model is already the injected provider object.
Trigger.dev and Inngest jobsruntime: trigger(...) or runtime: inngest(...). Farm talks to the selected external runtime and does not construct its SDK.
Eve and Cloudflare Agentsorigin for an external runtime, or Farm's managed development process. Agent SDKs remain application-owned.

Provider pages show the exact constructor and any integration-owned options that are still required.

Create callers

One setup for app routes and integrations

If the app uses file or plugin routes as well as integrations, use createApiClients once in src/lib/api.ts. Export the configured registry's type from a server-only module:

src/lib/integrations.ts
import { billing } from "../integrations/billing";

export const appIntegrations = { billing } as const;
export type AppIntegrations = typeof appIntegrations;

Here billing is the integration defined in the custom integration guide. appIntegrations is the real server-side object containing it. AppIntegrations is only a TypeScript description of that object: typeof does not create another integration, and as const preserves its literal types rather than freezing it at runtime.

Register that object with Farm once:

farm.config.ts
import { defineConfig } from "@farm.js/core";
import { appIntegrations } from "./src/lib/integrations";

export default defineConfig({
  integrations: appIntegrations,
});

This is where Farm registers the integrations and their routes. Keep provider instances and credentials in the server-only registry. Next, create callers for those already-configured services in the shared module; this does not create new provider instances:

src/lib/api.ts
import { createApiClients } from "@farm.js/core/client";
import { apiRoutes, type APIRouter } from "./api.generated";
import type { AppIntegrations } from "./integrations";

export const { api, apiClient } = createApiClients<APIRouter, AppIntegrations>({
  routes: apiRoutes,
  integrations: {
    data: { appName: "farm-dashboard" },
  },
});

The inputs have different jobs:

InputWhat it suppliesPresent in browser JavaScript?
APIRouterGenerated types for file and plugin routes.No; it is a type.
AppIntegrationsTypes for the configured integration operations.No; import type is erased.
apiRoutesGenerated paths and methods used to resolve app-route URLs, including dynamic params.Yes; it is schema-free route metadata, not server handlers.

The integrations.data option above is optional request metadata, not another integration registration. You can omit it. The factory returns api for server calls and apiClient for browser calls; both are exported from this one module.

App routes use paths such as apiClient.hello.get(...); integration calls use apiClient.integrations.billing.checkout.post(...) or api.integrations.billing.checkout.post(...). farm generate, development startup, and builds emit the route manifest. Import only the integration registry's type; the configured registry supplies integration metadata at runtime. No second createIntegrations() call is needed.

Shared setup does not unify result or transport behavior: integrations retain { data, error } results and server-side HTTP fallback, while app-route api calls require a Farm request and return { data, error, key } without HTTP fallback. See Integration callers for the full contract.

Integration-only setup

createIntegrations() remains supported and is not deprecated. There is no required migration: keep it when you prefer separate route and integration caller modules, or when only integration callers are needed. For separate modules, set integrations: false on the app-route createApiClients() setup and keep createIntegrations() for the integration callers. This does not change integration registration in farm.config.ts.

Existing caller options can also be a reason to keep createIntegrations(). The factories do not have interchangeable option signatures:

NeedcreateIntegrations()createApiClients()
Shared baseURL, headers, credentials, or data defaultsPass them in clientOptions.Pass them under integrations. These options alone do not require a separate factory.
Setup-level request, forwardHeaders, or separate server defaultsPass a separate serverOptions argument.No separate server-options argument; server integration calls support per-call overrides instead.
Explicit integration definitions or API contracts, for example in isolated packages or testsUse the source-map overload.Uses the configured integration registry.

For example, a request-scoped server helper can bind a request and its forwarding policy once:

src/lib/api.server.ts
import { createIntegrations } from "@farm.js/core/client";
import type { AppIntegrations } from "./integrations";

export function createRequestIntegrationApi(request: Request) {
  const { api } = createIntegrations<AppIntegrations>(
    { data: { appName: "farm-dashboard" } },
    { request, forwardHeaders: ["cookie", "authorization"] },
  );

  return api;
}

Keep this helper in server-only code and call it per request; do not cache a request-bound caller globally or import it into browser code. A serverOptions argument is not a bundler security boundary: private headers, tokens, and requests must stay out of shared client modules. Forward credentials only to trusted destinations.

The examples below use this integration-only setup and therefore omit .integrations. In an app using the shared factory above, reuse that pair and add .integrations to these integration call paths instead of creating another pair. Integration defaults such as data go inside its integrations option.

src/lib/api.ts
import { createIntegrations } from "@farm.js/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.

HTTP calls use the same API-root rules on both sides. For example, baseURL: "https://example.com/backend/v2" maps /api/billing/status to https://example.com/backend/v2/billing/status for browser calls and server HTTP fallback. A root-relative base such as /backend/v2 uses the browser origin or the server request's origin. Server-specific and per-call baseURL overrides follow the same rules. Registered local handlers still dispatch at their canonical route path without an HTTP round trip; a gateway prefix does not remount them. Only forward credentials to trusted HTTP destinations.

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 fieldHandles requests?Produces typed callers?Use it when
routesYesYes, 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.
endpointsYesYes, 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.
apiNoYesThe 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:

SurfaceWhat it is for
middlewareIntegration-owned request behavior for matchers outside a single endpoint.
providersReact provider metadata and optional wrapper components.
schemaDatabase models used by ctx.args.db through the integration ORM.
configSchema-validated config from defaults, env, input, and resolver output.
validateEarly checks before the integration starts.
setupBootstrapping work such as database checks or webhook registration.
readyPost-start work once the app is ready.
disposeShutdown cleanup.
logRuntime events for registration, request start, request end, request error, and lifecycle messages.
pluginsExtra Farm plugins that ship with the integration.
documentNavigationsRoute 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 database access.

src/integrations/local-demo.ts
import { defineIntegration, integrationRoute } from "@farm.js/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 "@farm.js/core/client";
import { localDemo } from "../integrations/local-demo";

export const { api, apiClient } = createIntegrations({
  localDemo,
});
client component
"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>;
}
server code
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 "@farm.js/core";
import { eve } from "@farm.js/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.

RuntimeFarm managesApplication uses
EveEve development process, /eve and workflow routes, Vercel build composition.agent/ files and useEveAgent().
Cloudflare AgentsWrangler 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.

Header defaults

createIntegrations supports static headers and sync or async header resolvers, just like createApiClients. Use a resolver when defaults must be read at call time rather than captured when the module loads:

import { createIntegrations } from "@farm.js/core/client";
import type { AppIntegrations } from "./integrations";

export const { api, apiClient } = createIntegrations<AppIntegrations>({
  headers: () => ({
    "Accept-Language":
      typeof document === "undefined" ? "en" : document.documentElement.lang || "en",
  }),
});

headers: async () => ({ ... }) is also supported. Each operation resolves its instance headers once, for both browser HTTP calls and server dispatch (including the server HTTP fallback). The existing separate factories and explicit-source overloads accept the same option.

Custom header precedence, lowest to highest, is: forwarded server request headers, instance defaults, operation-definition headers, then per-call headers. Overrides are case-insensitive; per-call headers remain plain objects. Farm still controls protocol/body headers, such as its integration marker and JSON/form encoding. Resolver failures return { data: null, error } without dispatching or fetching.

The second, server-options argument to createIntegrations can provide a different headers resolver for api; it replaces the first argument's header defaults rather than merging them. Keep any server-only version in a server-only module. A function or the server-options argument does not itself hide secrets from a browser bundle. For ordinary session cookies, retain Farm's existing request forwarding and browser credential behavior instead of copying secrets into shared defaults. The example's English server default overrides a forwarded language; omit that default if the incoming request should decide it.

Cancellation and deadlines

createIntegrations<AppIntegrations>({ timeoutMs: 10_000 }) sets one whole-call deadline for both callers. The separate server-options argument can override it for api. The same option works in the existing integration-only factories and in createApiClients's integrations options. A call can pass { signal, timeoutMs } as its second argument:

const controller = new AbortController();
const pending = apiClient.billing.checkout(
  { body: { priceId: "price_123" } },
  { signal: controller.signal, timeoutMs: 5_000 },
);
controller.abort();
const { error } = await pending;

The deadline includes header resolution, HTTP or local dispatch, and response decoding. 0 disables it; use an integer between 0 and 2147483647. Cancellation returns the normal { data: null, error } result. Farm deadlines produce an error named TimeoutError; ordinary controller.abort() produces AbortError. A custom abort reason is normalized to an error.

Direct server handlers receive the combined per-call and incoming request signal on their Request, just as HTTP calls receive the call signal. Cancellation stops waiting, not side effects: handlers must pass the signal to their own work when supported. A handler ignoring it can still finish or write data. For raw Response operations the deadline ends when the response is returned; use the signal to cancel subsequent HTTP body consumption.

Custom HTTP transport

createIntegrations<AppIntegrations>({ fetch: customFetch }) accepts the same fetch-compatible function as createApiClients. Both HTTP callers and the server HTTP fallback use it; registered local integration handlers still dispatch directly. Farm supplies the resolved URL and RequestInit, including the cancellation signal.

The separate server-options argument can replace fetch for api, and the existing integration-only factories accept it too. With combined route/integration callers, shared fetch is inherited unless integrations.fetch overrides it. Keep shared wrappers browser-safe and preserve request credentials, headers, and signals. Return a Web Response; do not put provider SDKs or server credentials in a shared module.

Shared lifecycle hooks

Integration callers accept the same onRequest, onResponse, and onError observers on the instance and in the second, per-call argument. Shared hooks run before per-call hooks; neither replaces the other. Per-call onResponse data is inferred from the operation's response type, while instance data is unknown. On failure the observer receives undefined data; the operation result still uses { data: null, error }.

Hooks cover browser HTTP, server HTTP fallback, and registered local dispatch. Response events include the path, method, request ID, timestamp, and available response/status. Integrations have one attempt (attempt: 0); these hooks do not introduce automatic retries. Resolver and cancellation failures are reported too, even when no HTTP request was sent.

onError runs on final failure. Hook return values are ignored; throwing or rejecting hooks are reported without changing the result, and promises are not awaited. Do not rely on observer completion for authorization, transactions, or required background work.

The separate server-options argument replaces the corresponding instance hook for api; createApiClients's integrations options can override shared defaults in the same way. Per-call hooks still compose after the effective instance hook. Shared observer modules must remain browser-safe and should log only intentional, non-sensitive metadata.

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

GroupBuilt-ins
PaymentStripe, Autumn, and Polar expose checkout, subscription, portal, webhook, entitlement, and billing snapshot patterns.
AuthBetter Auth, Auth.js, Clerk, Auth0, WorkOS, and Supabase expose routes, providers, session helpers, and auth middleware.
MessagingResend sends transactional mail, previews templates, and receives provider webhooks.
WorkflowsTrigger.dev and Inngest expose trigger, schedule, batch, status, and cancel APIs.
AgentsEve and Cloudflare Agents run beside Farm in development and compose with supported deployment targets.
API KeysUnkey can create, verify, revoke, update, and delete customer or service keys.
InterfaceUI registry entries scaffold shadcn-style integration screens when --ui is enabled.
DatabaseIntegration schemas give database-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.
  • Database 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.