---
title: "Custom Integrations"
description: "Build a first-class Farm integration with typed APIs, route handlers, lifecycle hooks, config validation, middleware, providers, database schemas, and runtime logs."
canonical_url: "https://mcp.farmjs.dev/docs/integrations/custom"
markdown_url: "https://mcp.farmjs.dev/docs/integrations/custom.md"
last_updated: "2026-10-20"
x_farming_labs_generated_preamble: true
---

# Custom Integrations
URL: /docs/integrations/custom
LLM index: /llms.txt
Description: Build a first-class Farm integration with typed APIs, route handlers, lifecycle hooks, config validation, middleware, providers, database schemas, and runtime logs.

# Custom Integrations

Use a custom integration when a service is bigger than a helper function. A good integration owns the contract between the app and the service: config, routes, typed callers, database models, lifecycle checks, request behavior, and any UI/provider setup the app needs.

Farm integrations are declared with `defineIntegration`. They are registered in `farm.config.ts`, then Farm turns them into pre-plugins during config resolution.

## Minimal integration

**src/integrations/acme.ts**

```ts
import { defineIntegration, integrationRoute } from "@farm.js/core";

export const acme = defineIntegration({
  category: "custom",
  type: "acme",
  instance: {},
  routes: [
    integrationRoute.get("/api/acme/status", {
      handler() {
        return Response.json({
          ok: true,
        });
      },
    }),
  ],
});
```

**farm.config.ts**

```ts
import { defineConfig } from "@farm.js/core";
import { acme } from "./src/integrations/acme";

export default defineConfig({
  integrations: {
    acme,
  },
});
```

The route is now mounted at `/api/acme/status`. Because it was created with `integrationRoute.get`, Farm can also derive the callable API shape.

## Choose the HTTP surface

`routes`, `endpoints`, and `api` are not three steps that every integration must configure. They
describe two different responsibilities:

```text
typed routes or endpoints -> mount HTTP handlers -> derive api and apiClient callers
plain route objects       -> mount HTTP handlers only
api                       -> describe callers only; no HTTP handler is mounted
```

| Field       | Runtime responsibility                                                       | Caller names come from                                       | Best use                                                                  |
| ----------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------- |
| `routes`    | Mounts a flat list of handlers.                                              | The route URL when you use `integrationRoute.*`.             | Most integrations. Start here.                                            |
| `endpoints` | Recursively flattens an object of handlers into `routes` and mounts them.    | The route URL when you use `endpoint.*`, not the object key. | Large integrations that are clearer when handlers are grouped by feature. |
| `api`       | Defines typed operations that call existing URLs. It never mounts a handler. | The keys in the `api` object.                                | Routes implemented elsewhere, or a deliberately customized caller tree.   |

A plain object in `routes` or `endpoints` still mounts its handler. Only the typed
`integrationRoute.*` and `endpoint.*` builders attach the operation metadata Farm needs to infer
`api` and `apiClient` request and response types.

Plain route objects accept every HTTP method when `method` and `methods` are omitted, or when
`method: "ALL"` is specified. Use an explicit method list or `integrationRoute.get/post/...`
to restrict which requests can reach a handler, especially webhooks and mutations.

The three formats below are alternatives. Each example exports an integration named `billing` and
uses the same registration and caller setup.

### Shared registration

The examples in this guide use the integration-only `createIntegrations()` factory, so the
`billing` key becomes the first segment after `api` or `apiClient`. If the app already uses
`createApiClients()` for file or plugin routes, [add the integration registry type to that same setup](/docs/api-client#integration-callers)
instead. Import the existing pair from `src/lib/api.ts` and use
`api.integrations.billing` / `apiClient.integrations.billing` for the calls below; do not create
a second pair. The integration definitions and registration stay the same.

Register the integration once:

**farm.config.ts**

```ts
import { defineConfig } from "@farm.js/core";
import { billing } from "./src/integrations/billing";

export default defineConfig({
  integrations: {
    billing,
  },
});
```

Create the browser and server callers once as well:

**src/lib/integrations.ts**

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

type AppIntegrations = {
  billing: typeof billing;
};

export const { api, apiClient } = createIntegrations<AppIntegrations>();
```

`apiClient` is the browser caller. `api` is the server caller; it dispatches directly to a
registered integration handler when possible and falls back to `fetch` when no runtime is
available.

### 1. `routes`: flat owned handlers

Use `routes` when the integration owns the HTTP handlers and a flat list is easy to read. This is
the recommended default.

**src/integrations/billing.ts**

```ts
import { defineIntegration, integrationRoute } from "@farm.js/core";
import { z } from "zod";

export const billing = defineIntegration({
  category: "payment",
  type: "acme-billing",
  instance: {},
  routes: [
    integrationRoute.post<"/api/billing/checkout", { priceId: string }, { url: string }>(
      "/api/billing/checkout",
      {
        body: z.object({ priceId: z.string() }),
        handler(_request, ctx) {
          return Response.json({
            url: `https://checkout.example/${ctx.input.body!.priceId}`,
          });
        },
      },
    ),
  ],
});
```

Farm mounts `POST /api/billing/checkout` and derives the caller from that URL.

**client code**

```tsx
"use client";

import { apiClient } from "../lib/integrations";

export async function startCheckout() {
  const result = await apiClient.billing.checkout.post({
    body: { priceId: "price_123" },
  });

  if (result.error) throw result.error;
  return result.data;
}
```

**server code**

```ts
import { api } from "../lib/integrations";

export async function startCheckoutOnServer() {
  const result = await api.billing.checkout.post({
    body: { priceId: "price_123" },
  });

  if (result.error) throw result.error;
  return result.data;
}
```

Both calls have typed input, typed `data`, and typed `error`. The Zod body schema also validates the
incoming request at runtime.
Integration clients decode both `application/json` and structured JSON media types such as
`application/problem+json`, so structured success and error payloads remain available to callers.

### 2. `endpoints`: grouped owned handlers

Use `endpoints` when the integration still owns the handlers, but an object is easier to organize
than a flat list.

**src/integrations/billing.ts**

```ts
import { defineIntegration } from "@farm.js/core";
import { z } from "zod";

export const billing = defineIntegration({
  category: "payment",
  type: "acme-billing",
  instance: {},
  endpoints: ({ endpoint }) => ({
    checkout: endpoint.post<"/api/billing/checkout", { priceId: string }, { url: string }>(
      "/api/billing/checkout",
      {
        body: z.object({ priceId: z.string() }),
        handler(_request, ctx) {
          return Response.json({
            url: `https://checkout.example/${ctx.input.body!.priceId}`,
          });
        },
      },
    ),
  }),
});
```

Farm flattens this object into `routes`, mounts the same HTTP handler, and generates the same typed
callers:

**client code**

```tsx
"use client";

import { apiClient } from "../lib/integrations";

export async function startCheckout() {
  const result = await apiClient.billing.checkout.post({
    body: { priceId: "price_123" },
  });

  if (result.error) throw result.error;
  return result.data;
}
```

**server code**

```ts
import { api } from "../lib/integrations";

export async function startCheckoutOnServer() {
  const result = await api.billing.checkout.post({
    body: { priceId: "price_123" },
  });

  if (result.error) throw result.error;
  return result.data;
}
```

The `checkout` object key is only an authoring label. The URL `/api/billing/checkout` determines the
derived caller path. If the key were `start` but the URL stayed the same, the caller would still be
`api.billing.checkout.post(...)`.

### 3. `api`: callers for existing handlers

Use `api` when this integration does not own the handler, or when you intentionally want caller
names that do not follow route URLs.

**src/integrations/billing.ts**

```ts
import { defineIntegration, endpoint } from "@farm.js/core";

export const billing = defineIntegration({
  category: "payment",
  type: "acme-billing",
  instance: {},
  api: {
    startCheckout: endpoint.post<{ priceId: string }, { url: string }>("/api/billing/checkout", {
      responseFormat: "json",
    }),
  },
});
```

This creates typed callers named `startCheckout`:

**client code**

```tsx
"use client";

import { apiClient } from "../lib/integrations";

export async function startCheckout() {
  const result = await apiClient.billing.startCheckout({
    body: { priceId: "price_123" },
  });

  if (result.error) throw result.error;
  return result.data;
}
```

**server code**

```ts
import { api } from "../lib/integrations";

export async function startCheckoutOnServer() {
  const result = await api.billing.startCheckout({
    body: { priceId: "price_123" },
  });

  if (result.error) throw result.error;
  return result.data;
}
```

Unlike `routes` and `endpoints`, `api` does **not** create `POST /api/billing/checkout`. The app or
another service must implement that URL.

If you provide `api` together with `routes` or `endpoints`, Farm still mounts the handlers, but the
explicit `api` object replaces all automatic caller derivation for that integration. Use that
combination only when you intentionally want caller names or operations that differ from the route
paths, and keep the declared methods and paths synchronized. Built-in integrations may use this
advanced combination to keep a stable public caller contract; most application integrations do not
need it.

The integration's `api:` field is a contract. It is different from the returned `api` value, which
is the server caller for that contract. `apiClient` is the browser caller.

## Full shape

The full interface is intentionally flat. Lifecycle hooks are top-level fields instead of being wrapped in a `lifecycle` object.

```ts
type IntegrationAuthoringShape = {
  category: string;
  type: string;
  instance: unknown;
  routes?: readonly unknown[]; // Flat HTTP handler definitions.
  endpoints?: object; // Grouped HTTP handler definitions.
  api?: object; // Caller definitions only; does not mount handlers.
  middleware?: readonly unknown[];
  providers?: readonly unknown[];
  documentNavigations?: readonly unknown[];
  schema?: object;
  config?: object;
  validate?: (ctx: unknown) => void | Promise<void>;
  setup?: (ctx: unknown) => void | Promise<void>;
  ready?: (ctx: unknown) => void | Promise<void>;
  dispose?: (ctx: unknown) => void | Promise<void>;
  log?: (event: unknown) => void | Promise<void>;
  plugins?: readonly unknown[];
};
```

In normal app code you do not need to write this type by hand. `defineIntegration` preserves the exact route, endpoint, config, and schema types for inference.

## Compose integration and application plugins

An integration can contribute the framework behavior it needs without asking every application to
repeat that plugin in `farm.config.ts`. The contributed plugin is still a normal `definePlugin()`
plugin. Farm binds the owning integration when it normalizes the config.

**src/integrations/better-auth.ts**

```ts
import { defineIntegration, definePlugin } from "@farm.js/core";
import { betterAuth } from "better-auth";

type BetterAuthInstance = ReturnType<typeof betterAuth>;

export function betterAuthSessionPlugin() {
  return definePlugin.forIntegration<BetterAuthInstance>()({
    name: "better-auth:session",
    runtime: {
      async context({ request, integration }) {
        return {
          session: await integration.instance.api.getSession({
            headers: request.headers,
          }),
        };
      },
    },
  });
}

export const auth = defineIntegration({
  category: "auth",
  type: "better-auth",
  instance: betterAuth(),
  plugins: [betterAuthSessionPlugin()],
});
```

`integration` contains the registration `key`, `category`, `type`, shared `instance`, and
`serverRuntime` ownership. It is required in `setup` and every server hook created through
`definePlugin.forIntegration<T>()`, so bound plugins do not need a runtime guard. A normal
application plugin has no owner; its `integration` field remains optional and `undefined` when the
plugin is registered globally.

`forIntegration<T>()` is a type-only binding: it does not wrap the plugin or add runtime data. It gives
every server hook the exact instance type and makes `defineIntegration` reject a plugin whose
expected instance does not match the configured `instance`. If the configured value is
`{ auth: betterAuth() }`, bind `{ auth: BetterAuthInstance }` and
`integration.instance.auth.api` is fully checked and autocompleted.

A bound plugin cannot be registered directly in the global `plugins` array. TypeScript rejects that
configuration because Farm cannot provide an owning integration there. Contribute it through the
matching integration's `plugins` array instead.

Prefer the return type inferred by `defineIntegration`. If a package must publish an explicit
integration type, preserve the instance as the third `FarmIntegration` type argument so its plugin
requirements remain checked:

```ts
import type { FarmIntegration } from "@farm.js/core";

type AuthResources = { auth: BetterAuthInstance };
type AuthIntegration = FarmIntegration<undefined, unknown, AuthResources>;
```

Using `FarmIntegration` without that instance argument intentionally leaves the instance unknown;
it does not silently accept an integration-bound plugin.

The application can still add cross-cutting plugins globally. Both kinds participate in the same
plugin pipeline:

**farm.config.ts**

```ts
import { defineConfig, definePlugin } from "@farm.js/core";
import { auth } from "./src/integrations/better-auth";

const requestId = definePlugin({
  name: "app:request-id",
  runtime: {
    after({ response }) {
      const headers = new Headers(response.headers);
      headers.set("x-request-id-policy", "app");

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers,
      });
    },
  },
});

export default defineConfig({
  integrations: {
    auth,
  },
  plugins: [requestId],
});
```

Farm normalizes the integration lifecycle plugin, its contributed plugins, and the application's
global plugins into one list. Normal plugin ordering still applies, including `enforce: "pre"` and
`enforce: "post"`. Contributed plugins inherit the integration's `serverRuntime` ownership, and Farm
rejects duplicate plugin names within one integration. Diagnostic tooling can inspect provenance
with `getFarmIntegrationPluginOwner(plugin)`.

The binding is server-only. Farm does not serialize the integration instance or private metadata
into a client plugin bundle. Only data explicitly returned from `client.public` can cross that
boundary.

## Config validation

Use `config` when an integration needs secrets or user options. Farm resolves config in this order:

1. `defaults`
2. values loaded from `env`
3. explicit `input`
4. `resolve(ctx)`
5. `schema` validation

**src/integrations/acme.ts**

```ts
import { defineIntegration } from "@farm.js/core";
import { z } from "zod";

const acmeConfigSchema = z.object({
  apiKey: z.string().min(1),
  webhookSecret: z.string().optional(),
  mode: z.enum(["test", "live"]).default("test"),
});

export const acme = defineIntegration({
  category: "custom",
  type: "acme",
  instance: {},
  config: {
    schema: acmeConfigSchema,
    env: {
      apiKey: "ACME_API_KEY",
      webhookSecret: "ACME_WEBHOOK_SECRET",
    },
    defaults: {
      mode: "test",
    },
  },
  validate(ctx) {
    if (ctx.integrationConfig.mode === "live" && !ctx.integrationConfig.webhookSecret) {
      throw new Error("ACME_WEBHOOK_SECRET is required in live mode.");
    }
  },
});
```

`ctx.integrationConfig` is the parsed config. If validation fails, the app fails during integration startup instead of later during a request.

## Lifecycle hooks

Lifecycle hooks receive the same base context plus the parsed `integrationConfig`, a `log` helper, and cleanup support.

| Hook            | When it runs                             | Use it for                                                                              |
| --------------- | ---------------------------------------- | --------------------------------------------------------------------------------------- |
| `validate(ctx)` | During integration init                  | Check API keys, required URLs, config combinations, and app prerequisites.              |
| `setup(ctx)`    | During integration init after validation | Prepare databases, register webhooks, create queues, warm clients, or schedule cleanup. |
| `ready(ctx)`    | When the app/plugin manager is ready     | Emit ready logs or start background listeners.                                          |
| `dispose(ctx)`  | During shutdown                          | Close clients, flush buffers, unregister listeners, or stop workers.                    |

```ts
export const acme = defineIntegration({
  category: "custom",
  type: "acme",
  instance: {},
  config: {
    schema: acmeConfigSchema,
    env: {
      apiKey: "ACME_API_KEY",
    },
  },
  async validate(ctx) {
    if (!ctx.integrationConfig.apiKey.startsWith("acme_")) {
      throw new Error("Invalid ACME_API_KEY.");
    }
  },
  async setup(ctx) {
    const interval = setInterval(() => {
      ctx.log.info("ACME heartbeat");
    }, 30_000);

    await ctx.cleanup(() => {
      clearInterval(interval);
    });
  },
  ready(ctx) {
    ctx.log.info("ACME integration ready", {
      key: ctx.key,
    });
  },
  dispose(ctx) {
    ctx.log.info("ACME integration disposed", {
      reason: ctx.reason,
    });
  },
});
```

`ctx.cleanup(callback)` registers cleanup that runs after `dispose`. Calling `ctx.cleanup()` with no callback runs the registered cleanups immediately.

## Typed routes

Within an integration, static paths take precedence over dynamic parameters, followed by
catch-all paths. For example, `/api/items/new` wins over `/api/items/[id]` regardless of
declaration order. Routes with equal specificity keep their declaration order.

Parameter names must be unique within a route. Use `/api/teams/[teamId]/members/[memberId]`,
not two `[id]` segments, so both identifiers reach the handler. Farm rejects `__proto__`,
`constructor`, and `prototype` as parameter names, including catch-all parameters. Validation
applies to plain routes, typed builders, grouped endpoints, and raw integration objects.

`integrationRoute` is the route factory. It supports `get`, `post`, `put`, `patch`, `delete`, `options`, and `head`.

```ts
import { defineIntegration, integrationRoute } from "@farm.js/core";
import { z } from "zod";

const createCheckoutBody = z.object({
  priceId: z.string(),
  successPath: z.string(),
  cancelPath: z.string(),
});

export const billing = defineIntegration({
  category: "payment",
  type: "acme-billing",
  instance: {},
  routes: [
    integrationRoute.post<
      "/api/billing/checkout",
      z.output<typeof createCheckoutBody>,
      { redirectTo: string }
    >("/api/billing/checkout", {
      body: createCheckoutBody,
      async handler(_request, ctx) {
        const checkout = await createCheckoutSession(ctx.input.body!);

        return Response.json({
          redirectTo: checkout.url,
        });
      },
    }),
  ],
});
```

For body and query validation you can use Zod, any parser with `parse`, any parser with `safeParse` or `safeParseAsync`, or a Standard Schema compatible validator through `~standard.validate`.

## Endpoints object

Use `endpoints` when you want to organize handler definitions in an object instead of a flat array.
Farm recursively flattens the object into `routes` and derives callers from each route URL. The
object keys are organizational; they do not control the caller namespace.

```ts
import { z } from "zod";

const checkoutBody = z.object({
  priceId: z.string(),
});

export const billing = defineIntegration({
  category: "payment",
  type: "acme-billing",
  instance: {},
  endpoints: ({ endpoint }) => ({
    checkout: endpoint.post<"/api/billing/checkout", { priceId: string }, { url: string }>(
      "/api/billing/checkout",
      {
        body: checkoutBody,
        handler(_request, ctx) {
          return Response.json({
            url: `https://checkout.example/${ctx.input.body?.priceId}`,
          });
        },
      },
    ),
    status: endpoint.get<"/api/billing/status", { active: boolean }>("/api/billing/status", {
      handler() {
        return Response.json({
          active: true,
        });
      },
    }),
  }),
});
```

The URLs in this example produce `api.billing.checkout.post(...)` and
`api.billing.status()`. Those names match the object keys because the final URL segments are also
`checkout` and `status`.

## Explicit API trees

Use `api` when routes are implemented elsewhere, when the integration only needs typed callers, or
when you intentionally want caller names that do not follow the URLs. An `api` operation never
registers an HTTP handler.

```ts
import { defineIntegration, endpoint } from "@farm.js/core";

export const acme = defineIntegration({
  category: "custom",
  type: "acme",
  instance: {},
  api: {
    profile: endpoint.get<{ id: string }, { id: string; name: string }>("/api/acme/profile", {
      responseFormat: "json",
    }),
    messages: endpoint.route(
      "/api/acme/messages",
      endpoint.get<{ items: string[] }>({
        responseFormat: "json",
      }),
      endpoint.post<{ text: string }, { id: string }>({
        responseFormat: "json",
      }),
    ),
  },
});
```

`endpoint.route(path, ...)` is useful when multiple methods share one path. A namespace with one method can be called directly and still keeps the method accessor.

If the integration also declares `routes` or `endpoints`, this explicit tree replaces the caller
tree Farm would derive from them. It does not replace or remove their HTTP handlers.

## Hooks around a route

Route-level `middleware`, `before`, and `after` run in this order:

1. Input validation
2. `route.middleware`
3. `route.before`
4. `handler`
5. `route.after`

```ts
integrationRoute.get("/api/acme/admin", {
  middleware: [
    {
      handler(_request, ctx) {
        ctx.req.set("startedAt", Date.now());
      },
    },
  ],
  before: [
    (_request, ctx) => {
      if (!ctx.data.tenantId) {
        return Response.json(
          {
            error: "tenantId is required",
          },
          {
            status: 400,
          },
        );
      }
    },
  ],
  handler() {
    return Response.json({
      ok: true,
    });
  },
  after: [
    (_request, ctx) => {
      ctx.response?.headers.set("x-integration", ctx.integration.type);
    },
  ],
});
```

`before` can return a `Response` to short-circuit the handler. `after` receives `ctx.response` for both normal handler responses and short-circuit responses, and can mutate it or return a replacement.

## Integration middleware

Top-level `middleware` is for request behavior that is not tied to one route. It uses a matcher and can short-circuit the request.

```ts
export const authGate = defineIntegration({
  category: "auth",
  type: "auth-gate",
  instance: {},
  middleware: [
    {
      matcher: "/dashboard/[section]",
      handler(_request, ctx) {
        if (!ctx.req.get("user.id")) {
          return new Response("Unauthorized", {
            status: 401,
          });
        }
      },
    },
  ],
});
```

Use top-level middleware for auth gates, tenant loading, rewrites, rate-limit checks, or provider-specific request enrichment.

## Context reference

Route handlers, route middleware, and route hooks receive `ctx` with these fields:

| Field                      | What it contains                                                                |
| -------------------------- | ------------------------------------------------------------------------------- |
| `request`                  | The web `Request` passed to the handler.                                        |
| `requestId`                | Request ID from headers or Farm-generated fallback.                             |
| `url`                      | Parsed `URL`.                                                                   |
| `pathname`                 | Current pathname.                                                               |
| `method`                   | HTTP method.                                                                    |
| `params`                   | Dynamic route params, including catch-all arrays.                               |
| `input.body`               | Parsed and validated body, if a body schema exists.                             |
| `input.query`              | Parsed and validated query, if a query schema exists.                           |
| `args.db`                  | Lazy ORM client for the integration schema.                                     |
| `args.getDb()`             | Promise-based ORM client resolver.                                              |
| `args.storage.getClient()` | Raw integration database client from `storage.client`, if configured.           |
| `data`                     | Small sanitized metadata from `createIntegrations({ data })` and per-call data. |
| `integration`              | Category, type, slot alias, and original `instance`.                            |
| `route`                    | Route kind, path, and methods.                                                  |
| `req`                      | Request-scoped key/value store shared with plugins and hooks.                   |
| `config`                   | Resolved Farm config.                                                           |
| `isDev` / `isProd`         | Runtime mode flags.                                                             |

`ctx.req.set(key, value, { exposeToPage: true })` can expose values to page props. Avoid exposing secrets.
`ctx.requestContext` remains as a deprecated compatibility alias for existing integrations.

## Database schema

Declare `schema` when an integration owns database records. Farm maps that schema to the integration ORM so route handlers and lifecycle hooks can use `ctx.args.db`. This path is separate from KV mounts read with `getStorage()`.

```ts
import { defineIntegration, defineSchema, integrationRoute } from "@farm.js/core";

const billingSchema = defineSchema({
  models: {
    billingAccount: {
      name: "billing_account",
      fields: {
        id: {
          type: "id",
          primaryKey: true,
        },
        ownerId: {
          type: "string",
          name: "owner_id",
          required: true,
          index: true,
        },
        status: {
          type: "enum",
          required: true,
          values: ["free", "active"],
          default: "free",
        },
        createdAt: {
          type: "datetime",
          required: true,
          default: "now",
        },
      },
      constraints: [
        {
          type: "unique",
          fields: ["ownerId"],
        },
      ],
    },
  },
});

export const billing = defineIntegration({
  category: "payment",
  type: "custom-billing",
  instance: {},
  schema: billingSchema,
  routes: [
    integrationRoute.get("/api/billing/account", {
      async handler(_request, ctx) {
        const account = await ctx.args.db.billingAccount.findFirst({
          where: {
            ownerId: String(ctx.data.ownerId),
          },
        });

        return Response.json({
          account,
        });
      },
    }),
  ],
});
```

If an integration does not define `schema`, `ctx.args.db` throws. Use `ctx.args.storage.getClient()` only when the integration needs the raw database or provider client.

## Database client config

The app supplies the integration database client once. `storage.client` is the current beta config name; a raw database object here is not returned by `getStorage()` and does not configure a KV mount:

**farm.config.ts**

```ts
import { defineConfig } from "@farm.js/core";
import { DatabaseSync } from "node:sqlite";
import { billing } from "./src/integrations/billing";

const sqlite = new DatabaseSync("farm.sqlite");

export default defineConfig({
  storage: {
    client: sqlite,
  },
  integrations: {
    billing,
  },
});
```

The integration code still uses `ctx.args.db`, not SQLite-specific APIs. If the app later switches to another supported client, the integration surface stays the same. See [Database and ORM Clients](/docs/integrations/orm-storage) for PostgreSQL, direct application ORM use, and adapter-owned databases.

## Providers

Use `providers` for client SDKs, context providers, or integration metadata that the app shell can compose.

```tsx
// src/components/acme-provider.tsx
"use client";

import type { FarmIntegrationProviderProps } from "@farm.js/core";

export function AcmeProvider({ children }: FarmIntegrationProviderProps) {
  return <>{children}</>;
}
```

Reference that client-safe module from the integration definition:

```ts
// farm.config.ts

export const acme = defineIntegration({
  category: "custom",
  type: "acme",
  instance: {},
  providers: [
    {
      name: "acme",
      type: "client",
      props: {
        publishableKey: process.env.ACME_PUBLISHABLE_KEY,
      },
      component: {
        module: "@/components/acme-provider",
        export: "AcmeProvider",
      },
    },
  ],
});
```

Farm statically imports the component into development and production client entries and composes
the same provider during server rendering. Use the standard `@/` source alias, a relative path, or
a package specifier; `export` defaults to `default`. Keep the provider module client-safe and
provider props public-safe. Secrets belong in server config and lifecycle hooks. Passing an opaque
function directly remains available to development and custom server-renderer callers, but a full
production build reports an actionable error because Farm cannot trace that function's imports
without also exposing the server configuration module.

## Logging

The `log` callback receives lifecycle and request events. Use it for observability, tests, and debugging integration behavior.

```ts
export const acme = defineIntegration({
  category: "custom",
  type: "acme",
  instance: {},
  log(event) {
    console.log("[integration]", event.phase, {
      type: event.type,
      route: event.route?.path,
      durationMs: event.durationMs,
    });
  },
});
```

Common phases are `registered`, `validate`, `setup`, `ready`, `dispose`, `request:start`, `request:end`, and `request:error`. Logging is observational: a synchronous throw or rejected promise from the callback is isolated and does not prevent integration startup, request handling, response delivery, or shutdown.

## Client and server usage

Automatic callers read the registered integration API manifest in the browser and the registered runtime on the server.

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

type AppIntegrations = {
  acme: typeof acme;
};

export const { api, apiClient } = createIntegrations<AppIntegrations>({
  data: {
    tenantId: "tenant_123",
  },
});
```

For library tests or isolated packages, pass explicit sources:

```ts
export const { api, apiClient } = createIntegrations({
  acme,
});
```

## Security checklist

- Validate config during startup with `config.schema` and `validate`.
- Validate all route body and query input before touching provider SDKs.
- Treat `ctx.data` as untrusted when it arrives from a browser.
- Do not expose secrets through provider props, page props, response bodies, or logs.
- Prefer `responseFormat: "json"` for typed callers and return structured errors.
- Keep webhook routes strict about method, body format, signature verification, and replay windows.
- Use `before` or route middleware for authorization checks close to the route they protect.
- Use `dispose` and `ctx.cleanup` for open handles, background listeners, and timers.

## Production checklist

- Define a stable `category` and `type`.
- Keep app namespace keys predictable in `farm.config.ts`.
- Add integration tests for valid input, invalid input, middleware short-circuiting, hooks, and client caller inference.
- Add database tests with real data when `schema` is present.
- Log enough request metadata to debug failures without logging secrets.
- Document env vars, route paths, API callers, and database models for app teams.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
