# Farm.js Docs
> Complete Farm.js framework documentation.
## Post-response Work
URL: /docs/after
Schedule short server work with after() without delaying the response.
# Post-response Work
Use `after()` for short server work that should start once Farm has finished the current response. Typical uses include analytics, audit events, cache warming, notifications, and non-critical cleanup.
Import it from the server-only entry:
```ts
import { after } from "@farm.js/core/after";
```
## Use it in a page
`after()` does not add its callback to the page's response time.
```tsx
import { after } from "@farm.js/core/after";
import { recordPageView } from "@/lib/analytics";
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
after(async () => {
await recordPageView({ productId: product.id });
});
return ;
}
```
Farm waits for a streamed response body to finish before starting the callback. Redirect and not-found responses use the same request lifecycle.
Cancelling a Web response also finishes that lifecycle, even if the stream producer's cleanup
is still pending. Farm starts `after()` callbacks and stops counting the response as active;
the caller of `cancel()` still receives the producer's cleanup promise, including any rejection.
## Use it in an API route
Keep work required for correctness in the handler. Schedule only the follow-up work that may happen after the client receives success.
```ts
import { after } from "@farm.js/core/after";
import { createEndpoint } from "@farm.js/core/api";
import { z } from "zod";
export const POST = createEndpoint(
{
method: "POST",
body: z.object({ orderId: z.string() }),
},
async ({ body }) => {
const order = await confirmOrder(body.orderId);
after(async () => {
await sendReceipt(order.id);
await recordOrderAnalytics(order.id);
});
return { ok: true, order };
},
);
```
The same API works in layouts, request middleware, server functions, and form actions because Farm creates one post-response queue for the whole request.
## Behavior
- `after(callback)` returns `void`; Farm owns when the callback starts.
- Callbacks start after the response finishes. For streaming responses, that means after the complete body is sent or the stream closes.
- Callbacks run one at a time in registration order.
- A callback may call `after()` again. The nested callback joins the end of the current queue.
- A callback error is logged, does not change the response, and does not prevent later callbacks from running.
- Request-local async context active at registration is preserved for the callback.
- Calling `after()` outside an active server request throws a clear runtime error.
On serverless adapters, Farm registers the queue with the provider's `waitUntil` lifecycle. On a Node server, Farm uses the response `finish` and `close` events. This keeps supported invocations alive without making the browser wait for the callback.
## Best practices
Capture stable values before scheduling work:
```ts
const userId = session.user.id;
const eventId = crypto.randomUUID();
after(() => analytics.track({ eventId, userId, name: "checkout.completed" }));
```
Do authentication, authorization, validation, and the primary database write before returning the response. The response is already committed when an `after()` callback runs, so the callback cannot safely change its status, headers, or cookies.
Make callbacks idempotent when they call external services. A deployment runtime may stop unexpectedly, and `after()` does not add durable storage, retries, or exactly-once delivery.
Use [Cron](/docs/cron) for periodic route invocation. Use the [Jobs Integration](/docs/integrations/jobs) for durable retries, long-running work, queues, visibility, or recovery. Use `after()` for bounded request follow-up work where low latency matters more than durable execution.
---
## API Client
URL: /docs/api-client
Call app API routes with apiClient.hello.get style inference, cache policies, invalidation, retries, callbacks, and optimistic updates.
# API Client
Call app API routes with apiClient.hello.get style inference, cache policies, invalidation, retries, callbacks, and optimistic updates.
## Create both callers once
**src/lib/api.ts**
```ts
import { createApiClients } from "@farm.js/core/api/client";
import { apiRoutes, type APIRouter } from "./api.generated";
export const { api, apiClient } = createApiClients({ routes: apiRoutes });
```
`@farm.js/core/api/client` is the renderer-neutral entry for typed API callers. The existing
`@farm.js/core/client` export remains supported for React apps that also use Farm's client hooks.
Define each endpoint once in a file route or plugin. This shared module imports only generated
paths/methods and types, never server handlers or credentials. Import `api` in server code and
`apiClient` in browser code; do not create another caller in either component.
Both app-route callers return `{ data, error, key }` and preserve the same input, output, method,
and dynamic-parameter inference. Keep secrets out of this shared module, including its options.
If the app also has integrations, add `AppIntegrations` as the second type argument to this same
factory. You do not need a second caller setup. See [Integration callers](#integration-callers).
The HTTP client uses the current origin and `/api` by default. To point every default client at another
API, configure it once in `farm.config.ts`:
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
api: {
baseURL: () => process.env.API_ORIGIN,
basePath: "/api",
},
});
```
`https://api.example.com` becomes `https://api.example.com/api`. A URL that already has a path,
such as `https://api.example.com/v1`, uses that path directly. `baseURL` and `basePath` may be sync
or async resolver functions; Farm evaluates them during config resolution and embeds only the
resulting public URL.
For a cross-origin API that uses cookies or HTTP authentication, pass the browser fetch credential
mode when creating the client:
```ts
export const { api, apiClient } = createApiClients({
baseURL: "https://api.example.com/v1",
credentials: "include",
});
```
Farm forwards `credentials` to every route request from that client. The API must also allow the
calling origin and credentialed requests through its CORS policy.
## Header defaults
Instance `headers` accepts a string-valued object or a function returning one. The function can
be synchronous or asynchronous; Farm calls it when an operation is called, not when the client
is created.
```ts
import { createApiClients } from "@farm.js/core/client";
import { apiRoutes, type APIRouter } from "./api.generated";
export const { api, apiClient } = createApiClients({
routes: apiRoutes,
headers: () => ({
"Accept-Language":
typeof document === "undefined" ? "en" : document.documentElement.lang || "en",
}),
});
// This call overrides the instance language without changing later calls.
await apiClient.hello.get({ headers: { "accept-language": "fr" } });
```
Use `headers: async () => ({ ... })` when obtaining defaults requires asynchronous work.
The exported `ClientHeaders` type describes all three forms. Static objects remain supported;
per-call `headers` remain objects, not resolver functions.
Header names are case-insensitive. App routes merge forwarded server request headers first,
then instance defaults, then per-call headers. Thus the example explicitly chooses English on
the server; omit its `Accept-Language` default when you want `api` to preserve the incoming
request's language. The existing server forwarding allowlist is unchanged.
Farm snapshots the resolved defaults once per operation call, before cache lookup or dispatch.
Cache hits still resolve headers; changed effective headers isolate the route client's cache
and in-flight deduplication. Retries reuse the same snapshot. A thrown error, rejected promise,
or invalid header produces the normal error result without fetching or returning cached data.
These defaults also apply to `api.integrations` and `apiClient.integrations` when enabled.
Providing `integrations.headers` replaces the shared header defaults for integration calls only;
it does not merge the two resolvers. See [integration header defaults](/docs/integrations#header-defaults)
for integration-specific precedence and separate server defaults.
A resolver is not a server-only boundary. Anything imported into this shared module may reach
the browser, and the resolver runs wherever its caller runs. Do not put provider credentials or
private environment values here. Read request-specific values inside the resolver or a
request-scoped server module, never by mutating a shared singleton with one user's credentials.
## Cancellation and deadlines
Set `timeoutMs` on `createApiClients` for a default deadline, or override it per call:
```ts
export const { api, apiClient } = createApiClients({
routes: apiRoutes,
timeoutMs: 10_000,
});
const controller = new AbortController();
const pending = apiClient.hello.get({}, { signal: controller.signal, timeoutMs: 2_000 });
controller.abort();
const result = await pending;
// result.error.code is "aborted" (or "timeout" when Farm's deadline expires).
```
The budget starts at call time and includes asynchronous header resolution, transport,
response decoding, and retry waits. `0` disables the deadline (also the default); accepted
values are integers from `0` to `2147483647`. A per-call `0` disables an instance deadline.
Keep `signal` per call, not on a shared long-lived instance.
Cancellation returns `{ data: undefined, error, key }` with status `0`, stops retries, skips
cached results when already aborted, and rolls back pending optimistic updates even when
`rollbackOnError` is false. Calls with a signal or deadline do not participate in in-flight
deduplication, so one caller cannot cancel another. Completed results can still use the cache.
Background revalidation retains its call budget until it finishes. A returned stream is handed
to the caller: the deadline ends at that handoff, not at the end of stream consumption; the
caller's signal still reaches the underlying HTTP request.
Local `api` dispatch combines the call signal with the incoming request signal. Handlers can
pass `request.signal` to cancellable work. Farm stops waiting when cancelled, but a handler or
custom transport that ignores the signal may continue running. Cancellation cannot undo
completed writes or external side effects.
These defaults also reach integration callers; `integrations.timeoutMs` overrides the shared
deadline. See [integration cancellation](/docs/integrations#cancellation-and-deadlines).
## Custom HTTP transport
Pass a fetch-compatible function when a caller needs an HTTP wrapper for testing or tracing:
```ts
export const { api, apiClient } = createApiClients({
routes: apiRoutes,
fetch: async (url, init) => {
const response = await globalThis.fetch(url, init);
return response;
},
});
```
The function has the same type as `globalThis.fetch` and receives Farm's resolved URL and
`RequestInit`, including headers, credentials, body, and cancellation signal. Return a normal
`Response` and forward the signal in wrappers. Farm still handles decoding, errors, retries,
and deadlines. Without this option, Farm uses global fetch as before. Supply a bound function
if your implementation requires a particular `this` value.
This replaces HTTP only: `apiClient` uses it, but server `api` still dispatches app routes
locally. Integration HTTP calls (including server HTTP fallback) inherit it;
`integrations.fetch` overrides it for integrations. Registered local integration handlers
do not use HTTP and are unchanged.
App-route caches stay private to instances with a custom transport, even with
`cache.scope: "shared"` or `credentials: "omit"`: wrappers can add identity that Farm cannot
see. If a wrapper changes users internally, also reflect that identity in the caller's
header resolver or create a new instance; Farm cannot detect hidden identity changes.
## Shared lifecycle hooks
Set `onRequest`, `onResponse`, or `onError` on the caller instance for shared logging or error
reporting. These are ordinary callbacks, not React hooks:
```ts
export const { api, apiClient } = createApiClients({
routes: apiRoutes,
onRequest(event) {
console.debug(event.method, event.path);
},
onResponse(_data, _error, event) {
console.debug("Status:", event.status);
},
onError(error) {
console.error(error.message);
},
});
await apiClient.hello.get(
{},
{
onError(error) {
// Handle this call's error in the UI as well as shared reporting.
console.debug("Could not load greeting:", error.message);
},
},
);
```
Shared hooks run first, then the corresponding per-call hook. Per-call route response types
remain inferred. Shared hooks use `unknown` data because one instance covers many routes;
`ClientLifecycleHooks`, `ClientRequestEvent`, and `ClientResponseEvent` are exported for reusable
observers. Return values are ignored. Promises are not awaited, and thrown/rejected shared
hooks are reported through `globalThis.reportError` (or console) without failing the call.
Asynchronous work may finish out of order even though callbacks are invoked shared-first.
`onRequest`/`onResponse` observe execution attempts, including retries and background refreshes,
not cache hits. Deduplicated requests share attempt events. `onError` runs once on final failure
for each logical call, including background failures; it does not run for each failed retry.
An attempt can fail before HTTP dispatch, such as header resolution or cancellation. A response
event observes a completed attempt; aborting inside it cannot retroactively cancel that result.
These hooks do not add retries or replace existing per-call success, settlement, or status hooks.
Shared defaults apply to integration callers too. `integrations.onRequest`, `onResponse`, and
`onError` replace the corresponding shared default for integrations; per-call hooks still compose.
Local server calls run their hooks on the server; HTTP callers run them where invoked. Keep
shared observers browser-safe and avoid logging credentials, request bodies, or personal data.
## Call a route
**Browser usage**
```ts
const result = await apiClient.hello.post({
body: { name: "Ada" },
});
if (result.error) {
console.error(result.error);
} else {
console.log(result.data.message);
}
```
If a route path contains a lowercase HTTP method segment such as `get`, `post`, or `delete` that
collides with a method on its parent route, the generated client exposes a leading-slash literal
alias so the two cannot be confused:
```ts
// Both src/app/api/users/route.ts and src/app/api/users/get/route.ts export GET.
const result = await apiClient["/users/get"].get();
```
The leading slash marks the whole key as a literal API path. This also works when the method-named
segment is in the middle of a colliding route, for example
`apiClient["/users/get/profile"].post(...)`. Non-conflicting paths keep their ordinary nested form.
A typed `HEAD` route is called with `.head()`. Its result keeps the same `{ data, error, key }`
shape, with `data` set to `undefined` because HTTP HEAD responses do not have a body.
Array-valued query inputs use repeated URL parameters. For example,
```ts
await apiClient.posts.get({ query: { tag: ["react", "vite"] } });
// GET /api/posts?tag=react&tag=vite
```
This is the same array representation that API route query schemas receive.
Query values must be strings, numbers, booleans, bigints, or arrays of those scalar values;
`null` and `undefined` entries are omitted. Nested objects are rejected instead of being silently
encoded as `[object Object]`. Untyped callers may also pass `URLSearchParams` directly when they
need its native ordering, duplicate-key, and encoding semantics.
## Scoped dynamic routes
`farm generate`, development startup, and production builds emit `apiRoutes` alongside
`APIRouter` in `src/lib/api.generated.ts`. Pass that manifest to enable parameter resolution:
```ts
import { createApiClients } from "@farm.js/core/client";
import { apiRoutes, type APIRouter } from "./api.generated";
export const { api, apiClient } = createApiClients({ routes: apiRoutes });
```
For `POST /api/projects/[projectId]/uploads/[uploadId]`:
```ts
const project = apiClient.projects.$params({ projectId: "project-123" });
const result = await project.uploads.post({
params: { uploadId: "upload-456" },
body: { title: "Design draft" },
});
if (result.error) throw result.error;
console.log(result.data?.title);
```
`$params()` returns a reusable, immutable caller and sends no request. Only the explicit HTTP
method executes the call. Bound parameters do not have to be repeated and cannot be overwritten
by a descendant call. You can bind the final parameter as well:
```ts
const upload = project.uploads.$params({ uploadId: "upload-456" });
await upload.post({ body: { title: "Updated draft" } });
```
The generated types describe registered methods, required parameters, validated input, and JSON
response data. The runtime manifest contains only paths and methods; importing it never imports
the config, server handlers, validators, or credentials into the browser. Existing static clients
without a manifest remain supported.
Binding preserves position: `/files/[id]/versions` uses
`apiClient.files.$params({ id }).versions.get()`, while `/files/versions/[id]` uses
`apiClient.files.versions.get({ params: { id } })`. Catch-all parameters use arrays:
`apiClient.docs.$params({ parts: ["guides", "start"] }).get()` for `/api/docs/[...parts]`.
Optional catch-alls can bind an empty object. Intermediate parameters must be bound before
accessing their children through the shorthand; existing bracket-pattern access remains available.
The client encodes parameter values, respects the configured API base URL/path, and rejects
missing/unknown parameters, unsafe values, unregistered methods, and ambiguous calls before
fetching. A supplied `undefined` ID never falls back to a collection request. Static server
routes still win: if `/api/uploads/stats` exists, calling `/api/uploads/[id]` with `id: "stats"`
throws a shadowing error rather than calling the wrong endpoint. Choose non-conflicting IDs or paths.
Resolved URLs participate in caching and invalidation, so different bound IDs remain separate.
Bind the final parameter before passing a route reference to a hook or invalidation helper when
you need one concrete resource; collection/detail overloads otherwise describe multiple inputs.
Plugin paths containing HTTP method names use a literal alias such as
`apiClient["/projects/get"].get()`. `$params` is reserved for the scope helper.
Both callers returned by `createApiClients()` support these scopes. Use `api` for local calls
during a Farm server request and `apiClient` for HTTP calls. For standalone HTTP calls from server
code, give `apiClient` a trusted absolute `baseURL` and explicitly forward only the credentials
the target needs. See [Server callers](#server-callers) for local-dispatch boundaries.
## Type-safe QUERY requests
A route that exports `QUERY` becomes a `.query()` caller. Its body and response are inferred from
the endpoint, just like the existing `.get()` and `.post()` callers:
```ts
const result = await apiClient.products.search.query(
{
body: {
filters: [{ field: "category", value: "tools" }],
limit: 20,
},
},
{
cache: {
policy: "stale-while-revalidate",
staleTime: 30_000,
},
},
);
if (!result.error) {
// Inferred from the QUERY handler response.
console.log(result.data.products, result.data.total);
}
```
TypeScript reports an error if `body` is missing or a filter has the wrong shape. Farm sends the
body as JSON and uses the `QUERY` method on the wire. Opt-in cache keys include the API origin,
path, URL query parameters, request body, `Content-Type`, and `Content-Encoding`. Multipart QUERY
requests need an explicit cache key because a generated multipart boundary cannot be represented
reliably before `fetch` sends the request.
Farm adds `Content-Type: application/json` when it serializes a JSON request body. Bodyless
requests do not receive that header, and an explicitly configured content type takes precedence
regardless of header casing.
## Upload files and consume progress streams
`toFormData()` retains the endpoint's body shape while sending files as real multipart fields. When
an endpoint returns `jsonStream()`, the generated client exposes a typed, single-consumer async
iterable. Concurrent `next()` calls on the same iterator are served in order, including items
already buffered in a transport chunk. Cancellation interrupts outstanding reads without waiting
for another chunk; `cancel()` itself still waits for producer cleanup:
```ts
import { toFormData } from "@farm.js/core/api";
const result = await apiClient.imports.post({
body: toFormData({
title: "Quarterly report",
file,
}),
});
if (result.error) {
throw result.error;
}
for await (const event of result.data) {
if (event.phase === "accepted") {
console.log(`Uploading ${event.bytes} bytes`);
} else {
console.log(`Imported ${event.imported} rows`);
}
}
```
Farm passes the `FormData` object directly to `fetch`, allowing the runtime to generate the required
multipart boundary. Do not set `Content-Type` manually. Stream items are decoded only as the
consumer advances the iterator, and `result.data.cancel()` aborts the response reader when the UI
no longer needs progress.
`jsonStream()` closes its source iterator at most once when cancelled or when a source/serialization
error occurs. A pending source read that finishes after cancellation is discarded. Explicit
cancellation waits for that cleanup and exposes cleanup failures; a source/serialization failure
keeps its original error even if cleanup also fails.
## Track mutations in React
`useMutation` gives generated API methods and Farm server functions the same pending, result, and
error lifecycle. API methods keep using the typed HTTP client underneath; they are not converted
into React Server Actions.
```tsx
"use client";
import { useMutation } from "@farm.js/core/client";
import { apiClient } from "@/lib/api";
export function CreateProductButton() {
const createProduct = useMutation(apiClient.products.post, {
request: {
invalidate: [[apiClient.products.get]],
},
});
return (
);
}
```
Use `mutate` for event handlers and `mutateAsync` when later code needs the resolved value:
```ts
const product = await createProduct.mutateAsync({
body: { name, category },
});
```
The return value includes `data`, `error`, `variables`, `status`, `pending`, and `reset`. Pass the
existing API-client cache, retry, invalidation, and optimistic options through `request`. A
server-function target honors `request.retry`, `request.optimistic`, and `request.invalidate`
with the same shapes; the remaining `request` options describe API-route transport and continue
to apply to API routes only. Local `optimistic` state on `useMutation` is separate from an API
cache update: it controls `mutation.data`, while `request.optimistic` updates shared cached
queries.
For a server-function target, optimistic updates and invalidations name explicit structured cache
keys, since the shared cache is the surface a server function can reach:
```tsx
const rename = useMutation(renameProduct, {
request: {
optimistic: {
update: [[["product", id], (current) => ({ ...current, name })]],
rollbackOnError: true,
},
invalidate: [["product", id]],
},
});
```
The updater applies to the shared client cache before the server function runs, so a
`useServerQuery` watching `["product", id]` renders the new name immediately. On success the
update commits and the invalidation travels the shared invalidation bus, exactly like a
server-declared `invalidates`; on failure `rollbackOnError` restores the previous entry, and
without it the touched entry is marked stale. Route-reference tuples such as
`[apiClient.products.get]` need an API caller's route identity and are skipped for
server-function targets; server-side keys the client cannot know about belong in the server
function's own `invalidates`.
Each local optimistic callback receives the latest scheduled mutation data, even when multiple
submissions occur before React rerenders. With `rollbackOnError: true`, a failed latest submission
restores the snapshot captured immediately before that submission's optimistic update.
### Pause offline submissions
By default a mutation dispatches regardless of connectivity and a submission with no network
fails after its retries. Opt into offline awareness with `networkMode: "online"`:
```tsx
const createProduct = useMutation(apiClient.products.post, {
networkMode: "online",
});
// createProduct.paused is true while the browser is offline;
// the submission dispatches automatically on the `online` event.
```
While the browser reports offline, a new submission waits in a `paused` state instead of
dispatching, and a dispatch that failed with a connectivity error while offline pauses and
rides the next reconnect instead of surfacing that error. `pending` stays true and `status`
stays `"pending"` for a paused submission; the new `paused` flag distinguishes "waiting for
connection" from "on the wire" so the UI can say so. Paused submissions resume in submission
order on the `online` event. `reset()` rejects paused submissions immediately rather than
leaving them waiting, and a disowned submission never dispatches after a later reconnect.
Optimistic state applied before the pause stays visible while waiting.
The pause window is in-memory: a reload drops paused submissions, so keep them short-lived and
surface `paused` to the user. `useFetcher` accepts the same option and exposes the same flag.
The default `networkMode: "always"` preserves existing behavior exactly.
For overlapping submissions, `status`, `data`, and `error` describe the latest submission.
Older completions do not replace its result or run completion callbacks. `pending` separately
tracks whether any submission is still running, so it can remain `true` after the latest one
succeeds or fails. `useFetcher` follows the same rules.
Mutation `onSuccess`, `onError`, and `onSettled` callbacks are non-awaited observers. A thrown
error or rejected promise is reported through `reportError` (or `console.error` as a fallback),
without changing the target result, rolling back a successful mutation, or decrementing pending
work again. Use the awaited `mutateAsync` result for required follow-up work. If a callback resets
the hook or submits again, the older submission does not run a stale `onSettled` callback.
`reset()` clears the displayed mutation state; it does not cancel the underlying work. Calls
started before reset still settle their own promises, but cannot change the new state or run its
completion callbacks. If you submit again, `pending` counts only calls started after reset.
`useFetcher.reset()` follows the same rule.
## Submit without navigation
Use `useFetcher` when a button or form should run an operation without changing the current route.
It accepts generated API methods, Farm server functions, and ordinary async functions:
```tsx
"use client";
import { useFetcher } from "@farm.js/core/client";
import { apiClient } from "@/lib/api";
export function CreateProductForm() {
const createProduct = useFetcher(apiClient.products.post, {
request: {
invalidate: [[apiClient.products.get]],
},
});
return (
{createProduct.error ?
{createProduct.error.message}
: null}
{createProduct.data ?
Created {createProduct.data.name}
: null}
);
}
```
The fetcher exposes `state` (`idle` or `submitting`), `status`, `pending`, `paused`, `data`, `error`,
`variables`, the active `formData`, `submit`, `submitAsync`, `Form`, and `reset`. It uses the same
optimistic updates, rollback, callbacks, typed errors, and API-client request options as
`useMutation`.
Generated API forms map text fields to a JSON `{ body: ... }` by default, or `{ query: ... }`
for GET/HEAD routes. For body methods, `` preserves
the FormData body, including repeated fields and files. A submit button's `formEncType` can
override the form encoding. Imperative `submit(formData)` / `submitAsync(formData)` also preserve
multipart data when it contains a File or Blob; text-only imperative input keeps the JSON default.
For a text-only multipart endpoint, pass `{ body: toFormData(values) }` directly or use a mapper
that returns `{ body: formData }`. Unsafe field names are filtered without mutating the original
FormData. `mapFormData` overrides these defaults, and server functions still receive FormData
directly. Native file-upload forms should declare `encType="multipart/form-data"` so their
pre-hydration fallback also transmits files.
Use `mapFormData` when the validated input needs coercion or a different shape:
```tsx
const quantity = useFetcher(apiClient.cart.post, {
mapFormData(formData) {
return {
body: {
productId: String(formData.get("productId")),
quantity: Number(formData.get("quantity")),
},
};
},
});
```
After hydration, `` prevents navigation and submits through the typed target. For a
server function, the function itself remains the native form action, preserving React's
progressive-enhancement path before JavaScript loads. Generated GET and POST API forms use the
real endpoint URL as their native fallback; a native fallback navigates to the endpoint response,
while the hydrated fetcher stays on the page.
If `mapFormData` throws, the target and optimistic callback are not called. The fetcher reports
`status: "error"`, updates `error`, and runs `onError`/`onSettled` with `variables: undefined`.
This also applies to `` and fire-and-forget `submit`; `submitAsync` rejects with the
same error. `formData` is cleared when submission finishes. Older work still contributes to
`pending`, but cannot overwrite this newer error; `reset()` clears it as usual.
Local mapping failures are wrapped in `FetcherInputError` from `@farm.js/core/client`, with
`code: "input_error"`, `status: 0` (no HTTP response), and the original thrown value in `cause`.
Its message preserves an original Error message or thrown string. Fetcher error types and
callbacks include this case alongside the target's existing errors. Narrow with
`error instanceof FetcherInputError` before handling provider-specific errors. Request-level
callbacks under `request` still receive only API-client errors, not local mapping failures.
## Client options
- cache: choose cache-first, network-only, or stale-while-revalidate; `persist` allows the
configured [client cache adapter](#persist-the-client-cache) to store the read.
- retry: retry transient failures with a non-negative integer count and a finite delay from 0 through 2,147,483,647 milliseconds.
- invalidate: mark typed route keys stale after mutations.
- optimistic: update cached query data before the server response returns.
- onRequest, onResponse, onSuccess, onError, onSettled, and onStatus: observe the full client lifecycle.
Lifecycle callbacks are non-awaited observers. Farm reports synchronous throws and rejected promises without changing the request result, retry decision, optimistic update, or settlement.
`onResponse` is a transport observer. If it throws or returns a rejected promise, Farm reports that
failure through the platform `reportError` hook (or the console fallback) without retrying or
changing the completed API result.
Use a structured cache key when an API response intentionally shares data with route data or a [`createServerQuery`](/docs/server-queries):
```ts
const { apiClient: publicApi } = createApiClients({ credentials: "omit" });
const product = await publicApi.products.get(
{ query: { id } },
{
cache: {
key: ["product", id],
scope: "shared",
policy: "stale-while-revalidate",
staleTime: 30_000,
},
},
);
```
Structured keys use Farm's route-data key contract. Default API cache keys include the API origin.
Same-origin and otherwise credentialed requests keep their cache private to the created client, and
changing explicit headers or credentials clears that private cache. This prevents a new client from
reusing a response produced under an earlier cookie identity without placing credential values in a
public cache key. Invalidate session-specific reads when the same client logs in or out.
Private caches follow the lifetime of their caller: on runtimes with `WeakRef`, the shared
invalidation channel does not keep an abandoned cache alive. Cleanup occurs during finalization
or a later invalidation, not on a guaranteed schedule. Keep reusable callers at module scope;
`gcTime` controls expiration of individual entries, not caller disposal: an expired entry is
evicted on its next read, and a periodic background sweep also removes expired entries that no
consumer is watching, so unread keys do not accumulate in long-lived sessions. Older runtimes
without `WeakRef` retain the existing strong subscription, so avoid repeatedly creating callers
there. Request-local server caches do not subscribe to the global invalidation channel.
Lazy expiry checks ownership only for the expired key and its aliases. Unrelated subscriptions
and requests are not scanned on each expired read; aliases and invalidation metadata remain
available while a related consumer or request still owns them.
Explicit entry deletion follows the same metadata cleanup rules: unowned aliases and invalidation
marks are released, while observed keys retain them until their last owner leaves. Deleting a key
that has never held an entry does not discard an explicit invalidation made before its first write.
Use `scope: "shared"` only for public data requested with `credentials: "omit"` and no custom
headers that intentionally shares a structured key with route data or another API client:
```ts
cache: {
key: ["catalog", "featured"],
scope: "shared",
policy: "cache-first",
}
```
Requests with `credentials: "omit"` and no custom headers may share by default because they carry
no browser identity. `scope: "client"` can keep those requests private as well. Credentialed or
header-carrying requests remain client-scoped even if `scope: "shared"` is supplied.
Set `cache.dedupeMs` to join identical requests started within that window. If an older request is
still running after the window expires, the newer request becomes the cache owner; the older result
still returns to its original caller but cannot replace the newer cached value.
The same ownership rule applies across different caller instances using a shared cache key.
The newest network read owns that entry even if it fails or is cancelled; an older pending
response cannot restore superseded data. Transport deduplication remains per caller, and private,
credentialed, custom-transport, and request-local server caches remain isolated.
Invalidation also protects against reads already in flight: their results still return to their
callers, but cannot become fresh cache entries after that key is invalidated. The next read can
fetch current data, even when invalidation and request startup happen in the same millisecond.
Unrelated cache keys are unaffected.
This includes mutation `invalidate` targets whose first read has not populated the cache yet.
Explicit keys and typed route references receive the same protection in private, shared, and
request-local server caches. Failed mutations leave those reads unchanged.
## Optimistic cache updates
Farm's cache lifecycle is intentionally familiar to React Query and TanStack Query users, but it is
implemented by Farm's own typed API client and shared cache. A mutation can update an existing
query result immediately, roll it back after an error, and invalidate it after the server responds.
```ts
const products = await apiClient.products.get(
{ query: { category } },
{
cache: {
key: ["products", category],
policy: "stale-while-revalidate",
staleTime: 30_000,
},
},
);
const createProduct = apiClient.products.post(
{
body: {
name,
category,
},
},
{
optimistic: {
update: [
[
products.key,
(current) => ({
...current,
products: [{ id: "optimistic", name, category }, ...(current?.products ?? [])],
}),
],
],
rollbackOnError: true,
},
invalidate: [products.key],
},
);
await createProduct;
```
The updater runs synchronously before the POST finishes. `products.key` preserves the cached
response type, so `current` is inferred from `apiClient.products.get`. You can also target a generated
route directly with `[apiClient.products.get, { query: { category } }, updater]`.
With `rollbackOnError: true`, Farm restores the exact previous cache entry when the mutation fails.
After a successful mutation, invalidation marks the key stale so mounted consumers or the next read
can load the canonical server result. Failed mutations do not invalidate known-good cached reads;
when rollback is disabled, only cache entries changed optimistically are marked stale so the next
read loads canonical data.
Use `invalidate: { targets: [[apiClient.products.get, input]], refetch: true }` to
refresh a previously cached API read in the background after a successful mutation. Farm reuses
that read's resolved route, input, key, cache policy settings, retry settings, and deadline, forcing
a fresh read instead of replaying the mutation. Duplicate targets refresh once. Explicit cache
keys work too; expired/missing entries or keys populated outside the API client are only invalidated
because they have no retained API read to replay.
Background refetch resolves current header defaults, does not reuse the old caller's abort signal
or per-call callbacks, and never repeats optimistic updates or mutation invalidations. Shared
lifecycle hooks still observe it. A failed refetch leaves the entry stale and does not fail the
already successful mutation. Read recipes expire or are removed with their cache entries.
## Cross-tab invalidation
By default, invalidations stay inside the tab that produced them: a mutation in one tab marks
keys stale there, while a second tab keeps its cached reads until its own focus or reconnect
refresh fires. Opt into same-origin cross-tab propagation with
`enableCrossTabCacheInvalidation`:
```ts
"use client";
import { enableCrossTabCacheInvalidation } from "@farm.js/core/client";
// Call once during client startup; returns a disposer.
enableCrossTabCacheInvalidation();
```
Invalidations from mutations, server functions, and explicit cache invalidation are posted to a
`BroadcastChannel` and applied in every other tab through the normal invalidation path, so
mounted stale queries there refetch on their own. Only invalidation keys cross the channel,
never cached data or responses, which leaves private and credentialed cache scoping untouched.
Each tab refetches through its own credentials.
The call is idempotent, safe during server rendering, and a no-op in environments without
`BroadcastChannel`. Pass `channelName` to isolate multiple Farm apps served from one origin;
enabling two different channel names in the same tab is an error.
## Live invalidation across clients
Cross-tab propagation stays inside one browser. To make a write in one user's session invalidate
mounted reads in other connected sessions, expose an app-owned server-sent event route. Authenticate
the request before creating the stream, and filter every key to the tenant or user allowed to
observe it:
```ts title="src/app/api/cache-events/route.ts"
import { createFarmCacheInvalidationStream } from "@farm.js/core/cache";
import { requireSession } from "../../../auth/server";
export async function GET(request: Request) {
const session = await requireSession(request);
return createFarmCacheInvalidationStream({
signal: request.signal,
filter: (key) => key.startsWith(`org:${session.orgId}:`),
});
}
```
Connect once during browser startup:
```ts
"use client";
import { enableLiveCacheInvalidation } from "@farm.js/core/client";
const dispose = enableLiveCacheInvalidation({
url: "/api/cache-events",
});
```
Farm batches invalidations emitted in the same microtask, sends only encoded cache keys, and applies
them through the ordinary client invalidation path. Mounted stale queries therefore refetch using
their existing inputs, cache policy, and current browser credentials. Native `EventSource`
reconnection handles transient disconnects; Farm's existing focus and reconnect revalidation covers
events missed while the browser was offline.
The stream is best-effort, not a durable event log: reconnects do not replay missed keys.
To bound memory for slow clients, Farm limits queued encoded output to 64 KiB and each pending
microtask batch to 1,024 distinct keys / 64 KiB of UTF-16 key data. Overflow (including an oversized
key or encoded batch) errors the stream and releases its listener and heartbeat; EventSource can
reconnect normally. Heartbeats are skipped while output is queued. Apps requiring guaranteed
delivery should use a durable event source and explicitly resynchronize reads after reconnecting.
The stream forwards events from the current server process. A multi-instance deployment needs a
shared pub/sub backplane feeding each instance's invalidation bus. That is intentionally outside the
first version of this API. The selected deployment target must also support a long-lived streaming
response. Do not expose a broad stream or put sensitive tenant data in unscoped cache keys: event
payloads contain keys, not cached values, but key names can still reveal information.
`enableLiveCacheInvalidation` shares one `EventSource` for repeated calls with the same URL and
credentials and closes it after the final disposer. Live events are not echoed through the optional
cross-tab bridge, because every connected tab receives the server event directly. Set
`withCredentials: true` only for a cross-origin endpoint that accepts credentialed EventSource
requests.
## Persist the client cache
The browser cache is in memory: a reload starts cold. Opt into persistence by pointing
`cache.client.adapter` at a client module and marking the reads that may be stored:
```ts title="farm.config.ts"
export default defineConfig({
cache: {
client: {
adapter: "./src/cache-adapter",
},
},
});
```
The path resolves from the project root and the module is bundled into the browser entry — the
server never imports it. Its default export implements five callbacks over any storage:
```ts title="src/cache-adapter.ts"
import { defineClientCacheAdapter, type PersistedEntry } from "@farm.js/core/client";
const PREFIX = "farm-cache:";
export default defineClientCacheAdapter({
async keys() {
return Object.keys(localStorage)
.filter((key) => key.startsWith(PREFIX))
.map((key) => key.slice(PREFIX.length));
},
async get(key) {
const raw = localStorage.getItem(PREFIX + key);
return raw ? (JSON.parse(raw) as PersistedEntry) : null;
},
async set(key, entry) {
localStorage.setItem(PREFIX + key, JSON.stringify(entry));
},
async delete(key) {
localStorage.removeItem(PREFIX + key);
},
async clear() {
for (const key of Object.keys(localStorage)) {
if (key.startsWith(PREFIX)) localStorage.removeItem(key);
}
},
});
```
Farm owns every policy decision — what may be stored, when writes flush, how entries load — and
the adapter is plain keyed storage. `set` is an upsert; there is no separate create. An adapter
over IndexedDB, OPFS, or a native bridge has the same shape, and the optional `setMany`/`getMany`
batch methods let it use one transaction per flush. `storageClientCacheAdapter` wraps any
Farm/unstorage-style key-value client into this contract, mirroring the server's
`storageCacheAdapter`, so one KV implementation can serve both caches — against two physically
separate stores.
### Nothing persists without opting in
Persisted data lives on the device, so persistence is allowlist-only. Mark reads where they are
declared:
```ts
export const catalogQuery = createServerQuery({
key: () => ["catalog", "featured"],
staleTime: "5m",
persist: true,
async handler() {
return loadCatalog();
},
});
```
```ts
await apiClient.products.get(
{ query: { category } },
{
cache: {
key: ["products", category],
policy: "stale-while-revalidate",
staleTime: 30_000,
persist: true,
},
},
);
```
Only successful, `persist`-flagged entries in the shared cache are written, debounced in the
background. Private and credentialed API caches never reach the adapter regardless of the flag.
`cache.client` also accepts `version` — a build or deploy id salted into every entry so a deploy
drops incompatible data — and an optional coarse `persistKey` filter on the engine.
### Lifecycle and limits
On startup, persisted entries load **stale-but-visible**: the data renders immediately and the
normal stale-while-revalidate path refreshes it on first read, so freshness semantics do not
change. Expired and version-mismatched entries are dropped during load. Deletes, garbage
collection, and `clear()` mirror to the adapter. If the adapter fails — quota, private browsing,
a broken driver — persistence disables itself for the session and the app continues memory-only;
the cache is disposable by design.
Call `clearPersistedCache()` on logout or any session change so the next visitor on a shared
device cannot warm-start into another user's data:
```ts
import { clearPersistedCache } from "@farm.js/core/client";
await clearPersistedCache();
```
Persistence stores confirmed reads only. Pending optimistic updates and in-flight mutations are
never written; a reload drops them.
## Result shape
App-route and integration callers both expose `data` and `error`, so callers can branch on
`result.error`. App-route results also include a typed cache `key`; integration results retain
their own error contract and do not include that cache key.
```ts
const result = await apiClient.hello.post({
body: {
name: "Ada",
},
});
if (result.error) {
console.error(result.error.status);
return;
}
console.log(result.data.message);
```
This makes client components easier to write because failed responses do not need to be caught with `try/catch` unless you want that behavior.
For app routes, if an HTTP error body is malformed, Farm still returns an `http_error` with the real status and
`Response`; the decoding failure is available as `error.cause`.
## Server callers
Import `api` from the same shared module. No endpoint imports, second factory, or request-bound
instance are needed:
```tsx
import { api } from "@/lib/api";
export default async function Page() {
const result = await api.hello.post({ body: { name: "Ada" } });
if (result.error) throw result.error;
return
{result.data?.message}
;
}
```
`api` resolves the current request and that app's registered routes at call time. It works in Farm
server pages, queries, actions, and API handlers in development and the default universal
production runtime. Constructing the pair at module scope is safe. Calling `api` in the browser
or outside an active Farm request throws an actionable error; it never silently falls back to HTTP.
The local caller uses the app's server API mount, including custom base paths. A public
`baseURL` pointing at another origin affects `apiClient`, not local dispatch. The current
request's `Cookie`, `Authorization`, and `Accept-Language` headers are inherited; shared options
and per-call headers can override them. Other headers are not implicitly forwarded. Do not place
private tokens in the shared module: read them in server code and pass them per call where needed.
Local calls use the same route matching, params, input/output validation, endpoint middleware,
body limits, and response decoding as HTTP routes. They still serialize request/response data;
they avoid a network round trip, not all serialization. Request cancellation is inherited.
Opt-in caches and in-flight requests are isolated by request and credentials, never shared between
users. Dynamic `$params()` scopes work identically for both callers.
### Direct-call boundaries
`api` is a direct endpoint caller, **not a replay of the full HTTP request pipeline**. It does not
run path-level HTTP middleware, redirects/rewrites, plugin request/response lifecycle hooks, or
deployment-layer checks. Put authorization and other rules required for both transports in the
endpoint's `middleware`. Use `apiClient` when an operation must go through HTTP middleware.
Response cookies and headers from a local endpoint do not automatically become headers on the
outer page response. Post-response work scheduled by an endpoint uses the enclosing request's
lifecycle. Integrations remain available under `api.integrations` and `apiClient.integrations`,
using their existing integration-dispatch semantics.
### Existing factories
`createAPIClient()` remains supported for an HTTP-only caller, including standalone scripts with
an absolute `baseURL`. `createServerAPIClient({ hello: { get: GET } })` remains supported for
explicit endpoint-function maps in server-only modules. It returns the supplied functions and
their raw results; passing only `{ request }` does **not** discover routes. Prefer the paired
factory for a shared module and a consistent `{ data, error, key }` app-route result.
The HTTP-only factory retains the same route and integration inference:
```ts
import { createAPIClient } from "@farm.js/core/client";
import type { APIRouter } from "./api.generated";
import type { AppIntegrations } from "./integrations";
const http = createAPIClient();
// Integration callers are under http.integrations..
const routesOnly = createAPIClient({ integrations: false });
// Only the app's route tree is exposed; no reserved integration caller namespace.
```
`integrations: false` disables only the caller namespace, not the configured providers or their
HTTP routes. These overloads are available from the published `@farm.js/core/client` entry;
there is no need to import framework internals or cast the options.
## Integration callers
Use the same shared module for file routes, plugin routes, and configured integrations:
**src/lib/api.ts**
```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({
routes: apiRoutes,
});
```
Export `AppIntegrations = typeof appIntegrations` from the server-only module containing the
registry passed to `farm.config.ts`'s `integrations` field. Import only its type here, not the
registry value or provider SDKs. The type describes existing integrations; it does not register
them. Farm supplies their caller metadata at runtime.
File and plugin routes keep their generated paths, such as `apiClient.hello.post(...)`.
Integrations live under the reserved `.integrations` namespace on both callers. For example, with
the `billing` integration from the [custom integration guide](/docs/integrations/custom#shared-registration):
```ts
// Browser code
const checkout = await apiClient.integrations.billing.checkout.post({
body: { priceId: "price_123" },
});
// Server code
const serverCheckout = await api.integrations.billing.checkout.post({
body: { priceId: "price_123" },
});
```
Integration-specific defaults belong in `integrations: { data, headers, ... }` in the shared
factory options. Only put browser-safe values there. Set `integrations: false` if the app does
not need the reserved namespace.
The shared factory does not change integration execution semantics. Integration calls return
`{ data, error }`, not the app-route `{ data, error, key }` cache contract. Server integration
calls dispatch to a registered handler when possible and can fall back to HTTP when local
dispatch is unavailable. App-route `api` calls instead require an active Farm server request and
never fall back to HTTP. Operations marked `isServer: true` remain available only through
`api.integrations`, not `apiClient.integrations`.
### Integration-only callers
`createIntegrations()` remains supported and is not deprecated. Existing apps
do not need to migrate. Use it when only integration callers are needed, or when you prefer to
keep them separate from app-route callers. It returns integration namespaces directly:
`apiClient.billing.checkout.post(...)` and `api.billing.checkout.post(...)`.
With `createApiClients`, those same calls need the
`.integrations` segment. Choose one setup for the shared module; do not create both pairs for
the same integrations. Switching factories requires updating the namespace and moving shared
integration defaults into the `integrations` option; it is not a drop-in rename.
The paired factory discovers configured integrations; it does not accept the integration-only
factory's explicit source map or separate server-options argument.
Options are another reason to choose the separate factory. Shared `baseURL`, `headers`,
`credentials`, and `data` defaults work with either setup: put them under `integrations` when
using `createApiClients()`. Keep `createIntegrations()` when you want setup-level `request` or
`forwardHeaders`, separate server defaults, or explicit source maps. The paired factory's
server integration calls still accept per-call overrides. See the [options comparison and
request-scoped example](/docs/integrations#integration-only-setup). Keep request-bound callers
and private server options in server-only code, not in the shared browser module.
For deliberately separate modules, use `createApiClients({ routes: apiRoutes,
integrations: false })` for app routes and `createIntegrations()` for integration
callers. Disabling the paired factory's namespace does not unregister integrations or their
HTTP routes; it only leaves integration access to the separate caller module.
## Server Function Form Actions
`createServerFn` pairs with `useServerFn` when a mutation is naturally a form action. Use `optimistic` to show the next UI state immediately, then let the server result replace it when the action completes.
Both hooks accept `retry` with the API client's shape, `{ count, delay }`, where `delay` is a
fixed wait or an `(attempt) => ms` backoff starting at attempt `1`. The default remains a single
attempt. Retries rerun the whole submission against the server function, so keep retried handlers
idempotent. `reset()` stops a waiting retry: the submission rejects with its last error and no
further attempts start.
For both `useServerFn` and `useAction`, `reset()` restores `initialResult` (or `null`), clears
the error, and returns the hook to idle. It does not cancel server work: promises from earlier
submissions still resolve or reject, but their completions no longer change hook state or invoke
completion callbacks. Requests started after the reset have their own pending count, so older
work cannot clear their pending state.
The `onSuccess`, `onError`, and `onSettled` options on these hooks are notification callbacks.
Thrown errors and rejected promises from them are reported through `reportError`, or the console
when unavailable, without replacing the server result/error or changing the pending count.
Callbacks are not awaited; put required work in the server function or explicitly await it after
the call. If `onSuccess` or `onError` synchronously resets the hook or starts another submission,
the superseded submission's `onSettled` callback is skipped. `throwOnFormError` still controls
whether `formAction` rethrows the server error, not a notification error.
**src/actions/todos.ts**
```ts
import { createServerFn } from "@farm.js/core/server-fn";
import { z } from "zod";
export const addTodo = createServerFn({
input: z.object({
title: z.string().min(1),
}),
output: z.object({
todos: z.array(
z.object({
id: z.string(),
title: z.string(),
}),
),
}),
async handler({ input, signal }) {
signal.throwIfAborted();
return {
todos: await db.todo.create({ data: input }),
};
},
});
```
`input` validates values before the handler runs. An optional `output` schema validates the
resolved handler result before it crosses the server-function boundary. Its parsed type becomes
the function's return type, and schema transforms are supported:
```ts
const PublicUser = z.object({
id: z.string(),
email: z.string().email(),
});
export const getUser = createServerFn({
input: z.object({ id: z.string() }),
output: PublicUser,
async handler({ input }) {
// PublicUser strips passwordHash before this result can reach the browser.
return db.user.findUniqueOrThrow({ where: { id: input.id } });
},
});
```
Output parsing also runs for direct server calls, form actions, and browser calls. Invalid results
reject the function just like invalid input. Keep the output contract narrow for private data;
do not rely on TypeScript alone to prevent an extra database field from being returned at runtime.
### Composable middleware
Use `createServerMiddleware` for server-only behavior shared by several functions, such as session
loading, authorization, transactions, rate limits, and auditing. Middleware can depend on other
middleware, and every context value is inferred by functions that install it.
```ts
import { createServerFn, createServerMiddleware } from "@farm.js/core/server-fn";
const withSession = createServerMiddleware({
async handler({ request, next }) {
if (!request) throw new Error("A request is required");
const session = await getSession(request);
if (!session.user) throw new UnauthorizedError();
return next({ context: { session } });
},
});
const withTransaction = createServerMiddleware({
middleware: [withSession],
async handler({ context, next }) {
return db.transaction((tx) => next({ context: { tx } }));
},
});
export const renameProject = createServerFn({
middleware: [withTransaction],
input: z.object({ projectId: z.string(), name: z.string().min(1) }),
async handler({ input, context }) {
// context.session and context.tx are both typed.
await requireProjectEditor(context.session, input.projectId);
return context.tx.project.update({
where: { id: input.projectId },
data: { name: input.name },
});
},
});
```
Dependencies run first and are de-duplicated by middleware identity. For
`middleware: [withTransaction, withAudit]`, a shared `withSession` dependency runs once. The chain
uses onion ordering: code before `await next()` runs from outer to inner, and code after it unwinds
from inner to outer.
Every middleware must call `next()` exactly once and return its result. Throw to reject a request;
middleware cannot silently skip the handler. Input validation finishes before the chain starts,
while output validation runs after the whole chain unwinds. Context is created on the server,
shallowly frozen, and never accepted from the browser.
Keep shared authentication in middleware, but still perform resource-specific authorization where
the resource is loaded. Derive identities, roles, tenant IDs, and rate-limit keys from the trusted
request or server state, never from unvalidated client fields. Middleware errors use the same
sanitized server-action error boundary as handler errors.
**src/components/todo-form.tsx**
```tsx
"use client";
import { useServerFn } from "@farm.js/core/server-fn/client";
import { addTodo } from "../actions/todos";
export function TodoForm() {
const action = useServerFn(addTodo, {
initialResult: { todos: [] },
rollbackOnError: true,
optimistic({ current, formData }) {
return {
todos: [
...(current?.todos ?? []),
{ id: "draft", title: String(formData?.get("title") ?? "") },
],
};
},
});
return (
);
}
```
The optimistic callback receives the raw input, `formData` for form submissions, and the current result. Return `undefined` when a submission should not change the optimistic result. Use `rollbackOnError` for reversible UI state; keep authorization and validation on the server function itself.
For `useServerFn` and `useAction`, `current` includes earlier optimistic transitions from the same
React batch. Three submissions that each return `(current ?? 0) + 1` from an initial result of `0`
therefore show `3`, even before React rerenders. The callback runs once per submission, including
in Strict Mode. A failed latest submission with `rollbackOnError` restores the result immediately
before that submission, including a previous optimistic value; a same-batch `reset()` restores
the initial snapshot for subsequent submissions. If an optimistic callback itself resets or starts
new work, the older transition cannot replace the reset or newer result. Reset still does not
cancel execution of an already-started submission.
When the function is called from the browser, `request` is the underlying Web `Request` and `signal` aborts with that request. A direct call made while rendering can inherit the current render request; a background or direct call outside request scope has no `request` and receives a stable, non-aborted signal. The same values are available to middleware. Pass `signal` to database or network clients that support cancellation.
Farm validates action origin metadata, accepted form/RSC content types, action ID shape, and request size before decoding an action. Browser calls use same-origin credentials and refuse redirects. Unexpected thrown values are logged on the server but become a generic `ServerActionError` in the browser, so secrets and stack traces are not serialized.
### Typed server function errors
Declare expected failures next to the input contract. `error` accepts only declared codes, validates
the public payload, and preserves the code, status, and data across the RSC action transport:
```ts
export const updateProduct = createServerFn({
input: updateProductSchema,
errors: {
NOT_FOUND: {
status: 404,
data: z.object({ id: z.string() }),
},
},
handler({ input, error }) {
const product = findProduct(input.id);
if (!product) {
return error("NOT_FOUND", { id: input.id });
}
return product;
},
});
```
`useServerFn`, `useMutation`, and `useFetcher` infer the declared error union. Narrow by `name` and
`code` to recover the exact payload:
```tsx
const update = useServerFn(updateProduct);
if (update.error?.name === "ServerFnFailure" && update.error.code === "NOT_FOUND") {
// id is inferred as string.
showMissingProduct(update.error.data.id);
}
```
Add an optional `message` only when it is safe to display publicly. Declared error data schemas must
support synchronous `parse()` or `safeParse()` because `error()` throws immediately. Hydrated calls
carry `status` inside the Flight error envelope; progressive form submissions also use it as the HTTP
status. Unexpected exceptions remain sanitized as a generic `ServerActionError`, without their
message, stack, or custom properties.
Action references identify which function to execute; they are not authorization tokens. Check authentication, roles, tenant ownership, and resource access inside every action that reads or changes private data.
## Production notes
- Keep generated API types committed or generated during CI.
- Prefer typed body/query schemas for mutations.
- Use server callers for secrets, auth cookies, and internal-only provider actions.
- Use invalidation after mutations that change cached route data.
- Keep optimistic updates scoped to UI state you can confidently roll back.
- Keep `serverActions.allowedOrigins` narrow and use API routes for intentionally cross-origin callers.
- Return typed expected failures; reserve thrown errors for unexpected failures.
- Add narrow output schemas to functions that return private database records.
---
## API Routes
URL: /docs/api-routes
Expose HTTP handlers from src/app/api and validate input with schemas before handler code runs.
# API Routes
Expose HTTP handlers from src/app/api and validate input with schemas before handler code runs.
## Route handlers
API route modules export HTTP methods. Farm discovers them, runs the route pipeline, and can generate typed client callers from the route shape.
Routes in one app source must have distinct URL shapes. For example, `/api/users/[id]` and
`/api/users/[slug]` conflict even though their parameter names differ. Parameters must have unique,
non-reserved names, and catch-alls must be the final segment. These checks also run in RSC builds.
If a project overrides a layer's equivalent dynamic shape, the project route replaces it completely,
so handlers do not receive parameters named by the lower-priority route.
**src/app/api/hello/route.ts**
```ts
import { createEndpoint } from "@farm.js/core/api";
import { z } from "zod";
export const POST = createEndpoint(
{
method: "POST",
body: z.object({
name: z.string().min(1),
}),
},
async ({ body }) => {
return Response.json({ message: "Hello " + body.name });
},
);
```
Use [`createApiClients()`](/docs/api-client) in one shared `src/lib/api.ts` to return `{ api, apiClient }`
from the generated router. Both callers reuse this route definition: `api` dispatches locally on
the server and `apiClient` sends HTTP requests. Endpoint middleware runs for both; outer HTTP
middleware is not replayed by direct calls. Never import endpoint modules into the shared caller file.
## MCP tool opt-in
When `@farm.js/mcp` is installed, a typed endpoint can opt into the authenticated MCP transport
with `mcp: true` or explicit tool metadata:
```ts
export const GET = createEndpoint(
{
method: "GET",
mcp: {
name: "list_projects",
description: "List projects visible to the current user.",
readOnlyHint: true,
},
},
async () => ({ projects: await listProjects() }),
);
```
The route still owns validation and endpoint middleware. The top-level `mcp` config separately owns
transport authentication in `farm.config.ts`. Alternatively, select explicitly pathed endpoint
instances in `mcp.tools` and return allowed tool names from `mcp.authorize` to control discovery
and invocation per caller. The same list can include standalone `defineTool()` definitions from
`@farm.js/mcp` for operations without an HTTP route. See the [API MCP guide](/docs/plugins/mcp)
for mixed composition, generated names, and security boundaries.
## Next-style exports
You can also manually export GET, POST, PATCH, and other handlers from the route file. Farm keeps this familiar while layering typed helpers around it.
Generated API client types follow runtime value exports. Comments, string examples, and type-only
exports named after HTTP methods do not create callable endpoints.
**src/app/api/status/route.ts**
```ts
export async function GET() {
return Response.json({ ok: true });
}
```
## Validation
> **Zod and standard schema**
>
> Endpoint and integration route inputs can use Zod or compatible standard-schema validators so the handler sees parsed input instead of raw unknown data.
## Route file shape
Farm follows the familiar route-file convention: each route lives in `src/app/api/**/route.ts` and exports one or more HTTP methods.
The equivalent `route.tsx`, `route.js`, and `route.jsx` filenames use the same discovery and generated-type behavior.
```txt
src/app/api/hello/route.ts -> /api/hello
src/app/api/users/[id]/route.ts -> /api/users/:id
src/app/api/files/[...path]/route.ts -> /api/files/*
```
Static route directory names may contain Unicode. Browsers percent-encode those path segments;
Farm decodes both the discovered route and request segment before matching them.
Defining the same method and path twice within one app source is an error, including in
production RSC builds. The error names both files. Project routes may still override matching
methods from a layer; other methods on the layer route remain available.
Use `createEndpoint` when you want input validation and typed client generation. Use plain `GET`, `POST`, `PATCH`, and friends when you want to handle the raw `Request`.
Plain handlers always receive that `Request`; parameter names such as `request`, `context`, or `ctx`
do not change the calling convention. Use `createEndpoint` for the parsed `{ body, query, headers }`
context.
`HEAD` follows normal HTTP semantics. A route can export a dedicated `HEAD` handler, otherwise Farm
uses its `GET` handler and returns the same status and headers without a response body.
This also applies to production RSC builds. They support `QUERY` exports, prefer an explicit
`HEAD` handler, and return `405` with an `Allow` header for unsupported methods.
## Body and query input
```ts
export const GET = createEndpoint(
{
method: "GET",
query: z.object({
q: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
}),
},
async ({ query }) => {
return Response.json({
q: query.q ?? "",
page: query.page,
});
},
);
```
Farm parses and validates `body`, `query`, and `headers` before middleware or handler code runs. Header schema keys use the lower-case names exposed by the Fetch `Headers` API. Invalid input returns a `400` response with structured validation issues.
Generated callers use the schema's **input** type for body and query values; middleware and
handlers receive its parsed **output** type. For example, a field declared as
`z.string().transform(Number)` is sent as a string and received by the handler as a number.
Defaulted fields may be omitted by the caller. The same distinction applies to multipart
`toFormData(...)` inputs, Standard Schema input/output types, and both `api` and `apiClient`
from `createApiClients()`. Validation still runs on the server; this does not send schemas or
transforms to the browser. Existing endpoints without transforms retain their input types.
Malformed `application/json` and `application/*+json` bodies return `400` before endpoint middleware
or handler code executes, including when the endpoint does not declare a body schema. Empty declared
JSON receives the same response for non-QUERY methods. A bodyless `QUERY` remains valid
because QUERY requires `Content-Type` and generated clients attach `application/json` even when the
endpoint declares no body. Malformed `multipart/form-data` also returns `400` instead of becoming an
absent body; an empty `application/x-www-form-urlencoded` body is a valid empty form and parses as an
empty object. Callers that omit `Content-Type` retain the permissive JSON parsing path for
compatibility.
If an upload is aborted while Farm is buffering its body, Farm rejects it before invoking the
endpoint. This does not roll back work in a handler that has already started.
On Node, finishing an upload is not an abort: only an interrupted upload or an early response
disconnect cancels the request signal.
## HTTP QUERY
Use the standardized [`QUERY` HTTP method](https://www.rfc-editor.org/rfc/rfc10008.html) when a
read operation needs structured request content that is too large or sensitive for a URL. Like
`GET`, `QUERY` is safe and idempotent; unlike `GET`, its request body has defined semantics.
**src/app/api/products/search/route.ts**
```ts
import { QUERY as createQueryEndpoint } from "@farm.js/core/api";
import { z } from "zod";
export const QUERY = createQueryEndpoint(
{
body: z.object({
filters: z.array(z.object({ field: z.string(), value: z.string() })),
limit: z.number().int().min(1).max(100).default(20),
}),
},
async ({ body }) => {
const products = await searchProducts(body.filters, body.limit);
return { products, total: products.length };
},
);
```
The helper infers the validated `body` inside the handler and exposes the same input and response
types to the generated client. QUERY requests must include a `Content-Type` header; Farm's generated
client sets `application/json` automatically. A raw handler is also valid:
```ts
export async function QUERY(request: Request) {
const search = await request.json();
return Response.json(await searchProducts(search));
}
```
Keep `QUERY` handlers read-only. Use `POST`, `PATCH`, or another unsafe method when the operation
changes server state. A server can advertise accepted query media types with an `Accept-Query`
response header. Cross-origin browser requests use a CORS preflight, so include `QUERY` in the
configured `cors.methods` list when that list is restricted.
## Uploads and streaming results
Use `multipart()` when an endpoint accepts files. Farm parses the request as `FormData`, preserves
`Blob`/`File` values and repeated fields, then runs the same body-schema validation used for JSON:
```ts
import { createEndpoint, jsonStream, multipart } from "@farm.js/core/api";
import { z } from "zod";
const importBody = multipart(
z.object({
title: z.string().min(1),
file: z.custom((value) => value instanceof Blob),
}),
);
type ImportEvent = { phase: "accepted"; bytes: number } | { phase: "complete"; imported: number };
export const POST = createEndpoint(
{
method: "POST",
body: importBody,
},
async ({ body }) => {
async function* importEvents(): AsyncGenerator {
yield { phase: "accepted", bytes: body.file.size };
const imported = await importRows(body.file);
yield { phase: "complete", imported };
}
return jsonStream(importEvents());
},
);
```
`jsonStream()` uses newline-delimited JSON (`application/x-ndjson`). It sends each typed event as
soon as the source yields it, respects response backpressure, cancels the source when the reader
disconnects, and defaults to `Cache-Control: no-store`. Use ordinary `Response` objects for binary
downloads or protocols that are not JSON event streams.
The client decoder rejects malformed NDJSON with its original `SyntaxError` and cancels the
source without waiting for cleanup or an unread response clone. Explicit stream cancellation
and early iterator return still await the producer's cleanup.
When Farm bridges a streamed `Response` to Node, a failed response write also cancels the
upstream body. Put producer cleanup in the stream's `cancel()` callback. Farm preserves the
original write error and does not wait indefinitely for that cleanup to finish.
Disconnects follow the same rule, including when the connection closes under backpressure:
Farm releases its reader and response listeners without waiting for app-owned cancellation work.
## Endpoint middleware
Put plain async functions in `middleware`. There is no middleware factory and no `next()` callback. Functions run in declaration order after endpoint input validation.
**src/app/api/projects/[id]/route.ts**
```ts
import { createEndpoint, type EndpointMiddlewareContext } from "@farm.js/core/api";
import { z } from "zod";
type Session = {
user: { id: string; roles: string[] };
};
async function requireAuth({ request }: EndpointMiddlewareContext) {
const session = await getSession(request);
if (!session?.user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return { session: session as Session };
}
const requireRole =
(role: string) =>
async ({ context }: EndpointMiddlewareContext<{ session: Session }>) => {
return context.session.user.roles.includes(role);
};
async function loadProject({
body,
context,
params,
}: EndpointMiddlewareContext<{ session: Session }, { name: string }>) {
const project = await db.project.findUniqueOrThrow({
where: {
id: String(params.id),
ownerId: context.session.user.id,
},
});
return { project };
}
export const PATCH = createEndpoint(
{
method: "PATCH",
body: z.object({ name: z.string().min(1) }),
middleware: [requireAuth, requireRole("admin"), loadProject],
},
async ({ body, context }) => {
// context.session and context.project are inferred from middleware returns.
return db.project.update({
where: { id: context.project.id },
data: { name: body.name },
});
},
);
```
Middleware return values have deliberate control-flow meaning:
| Return value | Result |
| ------------ | ---------------------------------------------------------------------------------------- |
| Plain object | Shallow-merges its properties into typed `context` for later middleware and the handler. |
| `true` | Continues without adding context. |
| `false` | Stops and returns Farm's JSON `403 Forbidden` response. |
| `Response` | Stops and returns that response unchanged. Use this for custom status, body, or headers. |
Only literal `false` is the default denial signal. Returning `null`, `undefined`, an array, or another value is an error, which catches forgotten returns instead of silently skipping authorization. Context is shallowly frozen, duplicate keys are rejected, and `__proto__`, `constructor`, and `prototype` cannot be provided as context keys.
Endpoint middleware is local to one `createEndpoint` declaration and can use its validated input. Use `src/app/**/middleware.ts` for path-level behavior shared by many routes, such as request tracing, common headers, or an early rewrite before endpoint parsing.
> **Authorization boundary**
>
> Derive users, roles, tenants, and rate-limit identities from the trusted `Request` or server state. Middleware context is created only on the server and is never accepted from client input. Keep resource-specific permission checks close to the resource query even when shared authentication runs in middleware.
## Typed expected errors
Declare failures that are part of an endpoint's public contract, then return them through the typed
`error` function:
```ts
export const POST = createEndpoint(
{
method: "POST",
body: z.object({
name: z.string().min(1),
}),
errors: {
duplicate: {
status: 409,
message: "A product with this name already exists",
data: z.object({
existingId: z.string(),
}),
},
forbidden: {
status: 403,
data: z.object({
permission: z.string(),
}),
},
},
},
async ({ body, error }) => {
const existing = await findProductByName(body.name);
if (existing) {
return error("duplicate", {
existingId: existing.id,
});
}
return createProduct(body);
},
);
```
Error codes and payloads are checked in the handler and carried into the generated API client:
```ts
const result = await api.products.post({
body: { name },
});
if (result.error?.code === "duplicate") {
// existingId is inferred as string.
showExistingProduct(result.error.data.existingId);
}
```
Farm validates the failure payload before returning a JSON error response. Declared messages and
payloads are public, so do not include secrets. Undeclared exceptions remain unexpected server
errors and are not converted into a declared failure. Endpoints without an `errors` declaration
keep the existing `Error` client type.
The generated client makes this endpoint RPC-like to call, but the transport remains a regular HTTP
request and JSON error response. Existing endpoint definitions using `schema` and `fail` continue to
work as deprecated aliases; use `data` and `error` in new code for consistency with server functions.
## Client inference
Routes become client namespaces from their path:
```ts
await api.hello.post({
body: {
name: "Ada",
},
});
await api.users.get({
query: {
limit: 10,
},
});
```
The exact generated shape comes from route generation. Body and query schemas become typed caller input, and path segments become the nested `api.users.get` style namespace. During `farm dev`, Farm regenerates route/API types when page or API route files are added, changed, or removed. Run `farm generate` when you want the same refresh outside the dev server.
## Declare invalidation with the mutation
When every caller of a mutation makes the same data stale, declare that relationship on the
endpoint instead of repeating client-side invalidation:
```ts
export const PATCH = createEndpoint(
{
method: "PATCH",
body: z.object({
id: z.string(),
name: z.string().min(1),
}),
invalidates: ({ body }) => [
{ key: ["product", body.id] },
{ key: ["products", "list"] },
{ tag: "products" },
{ path: "/products" },
],
},
async ({ body }) => {
return db.product.update({
where: { id: body.id },
data: { name: body.name },
});
},
);
```
Farm applies declared keys, tags, and paths to the server cache after the handler succeeds. Normal
`api.products.patch(...)` callers also receive the key invalidations through response metadata, so
matching browser queries become stale without repeating an `invalidate` option. A response with a
status of 400 or higher, or middleware that stops before the handler, does not invalidate.
The resolver receives validated body, query, and headers plus the accumulated typed middleware
context. Invalidation is declarative rather than inferred: database writes do not reliably reveal
every affected query. Existing client-side `invalidate` options remain supported for
caller-specific cache relationships, and handlers can continue calling `invalidate(...)` or
`revalidatePath(...)` directly.
## When to use integrations instead
Use API routes for app-owned endpoints. Use integrations when a provider or feature needs a package-like surface: config validation, lifecycle hooks, database schemas, middleware, providers, and typed callers bundled together.
---
## Built-in Authentication
URL: /docs/auth
Enable Farm's built-in email/password authentication with one config key, server helpers, and a React hook.
# Built-in Authentication
Farm Auth is a built-in Farm framework feature for applications that need ordinary email/password authentication without configuring a provider instance. It is enabled through the top-level `auth` framework config—not through `integrations.auth`.
The implementation lives in the first-party `@farm.js/auth` runtime package so database drivers and the authentication engine do not bloat `@farm.js/core`. Farm loads that package only when built-in auth is enabled.
Install the optional runtime:
```bash
pnpm add @farm.js/auth
```
Then enable the built-in feature:
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
auth: true,
});
```
That one key:
- enables email/password sign-up and sign-in;
- mounts `GET` and `POST` auth actions below `/api/auth`;
- creates and refreshes database-backed sessions;
- exposes Farm server helpers and React APIs;
- uses `.farm/auth.sqlite` in local development;
- uses `DATABASE_URL` in production.
`auth: { enabled: true }` is also accepted when keeping feature switches in object form. `auth: true` is the canonical shorthand.
## Start from a complete app
The [Farm.js Auth Starter](https://github.com/farming-labs/farmjs-auth-starter) demonstrates the complete built-in path: email/password forms, server helpers, the React hook, protected middleware, local SQLite, and production Postgres configuration.
```bash
git clone https://github.com/farming-labs/farmjs-auth-starter.git
cd farmjs-auth-starter
pnpm install
pnpm dev
```
## Server helpers
Read the current session or user from a Server Component, server function, middleware, or route handler:
```ts
import { auth } from "@farm.js/auth/server";
const session = await auth.session();
const user = await auth.user();
```
Both return `null` for an anonymous request. Use the same methods with `required: true` when the request must be authenticated:
```ts
const user = await auth.user({ required: true });
```
An anonymous request throws a `401` response with the stable code `FARM_AUTH_REQUIRED`. There is no separate `requireUser` API to learn.
Farm Auth does not configure or inspect pages. Authentication answers who made the current request; each page, server function, or API route decides what that user may do.
## React hook
Use `useAuth` in a Client Component:
```tsx
"use client";
import { useAuth } from "@farm.js/auth/client";
export function AccountMenu() {
const { user, isPending, signOut } = useAuth();
if (isPending) return Loading…;
if (!user) return Sign in;
return ;
}
```
The client entry also exports `signIn`, `signUp`, `signOut`, and `getSession` for forms that do not need the hook:
```ts
import { signIn, signUp } from "@farm.js/auth/client";
await signUp({
name: "Ada Lovelace",
email: "ada@example.com",
password: "correct-horse-battery-staple",
});
await signIn({
email: "ada@example.com",
password: "correct-horse-battery-staple",
});
```
## Configuration
The default is intentionally useful. Add options only when application policy differs:
```ts
export default defineConfig({
auth: {
enabled: true,
appName: "Acme",
emailAndPassword: {
requireEmailVerification: true,
minPasswordLength: 12,
},
session: {
expiresIn: 60 * 60 * 24 * 30,
updateAge: 60 * 60 * 24,
},
},
});
```
| Option | Default | Purpose |
| ------------------------------- | ------------------- | ------------------------------------------------- |
| `enabled` | `true` | Temporarily disable configured auth. |
| `appName` | `"Farm app"` | Name used in auth metadata and messages. |
| `basePath` | `"/api/auth"` | Route prefix for auth actions. |
| `emailAndPassword` | enabled | Email verification and password length policy. |
| `session` | 7 days | Session lifetime and refresh interval in seconds. |
| `database.url` | `DATABASE_URL` | Explicit Postgres connection string. |
| `database.path` | `.farm/auth.sqlite` | Local SQLite path. |
| `database.migrateInDevelopment` | `true` | Update the local schema lazily on first auth use. |
This interface stays at application-policy level. It does not expose a Better Auth instance or require Better Auth imports in application code.
`basePath` accepts a stable application pathname, with or without its leading slash. URLs, query
strings, fragments, dot segments, backslashes, control characters, and encoded path separators are
rejected.
If `basePath` is customized, create matching client helpers once:
```ts
import { createFarmAuthClient } from "@farm.js/auth/client";
export const { useAuth, signIn, signUp, signOut } = createFarmAuthClient({
basePath: "/account/auth",
});
```
## Database and deployment
Local schema creation happens lazily when auth is first used. Loading `farm.config.ts` and running `farm build` never connect to the database and never run migrations.
For production, set:
```bash
DATABASE_URL=postgres://...
FARM_AUTH_SECRET=...
FARM_AUTH_URL=https://example.com
```
`AUTH_SECRET` and `BETTER_AUTH_SECRET` remain accepted as secret aliases.
On Vercel the auth URL is inferred when `FARM_AUTH_URL` is absent: a production
deployment uses `VERCEL_PROJECT_PRODUCTION_URL`, the project's canonical domain, and a preview
uses its own `VERCEL_URL`. The deployment and branch hostnames stay trusted alongside it, so a
deployment URL still works for testing. Set `FARM_AUTH_URL` explicitly when the app is served
from a domain Vercel does not report, such as a proxy in front of the deployment - the auth
routes reject any origin that is neither the base URL nor a trusted host.
Apply the production schema before serving traffic:
```bash
farm auth migrate
```
The migration command needs `DATABASE_URL`, but it does not require the runtime secret.
To read the statements before they touch the database, ask for them instead of applying:
```bash
farm auth migrate --dry-run
farm auth migrate --write auth-schema.sql
```
Both print or save exactly what `farm auth migrate` would run and change nothing. Better Auth owns this schema, so the statements come from its migrator rather than from the table declarations a plugin makes to `farm migrate`.
## Extend with Better Auth
The built-in path is intentionally opinionated, but it does not replace the existing Better Auth integration. Both approaches are supported:
| Need | Configuration owner |
| -------------------------------------------------------------------- | ---------------------------------------------- |
| Email/password auth with Farm defaults, helpers, and hooks | Top-level `auth: true` |
| Better Auth plugins, adapters, providers, callbacks, or instance API | `integrations.auth` with an app-owned instance |
Choose the integration path when the application should own the complete Better Auth configuration:
```ts
import { defineConfig } from "@farm.js/core";
import { betterAuth } from "@farm.js/better-auth";
import { auth } from "./src/lib/auth";
export default defineConfig({
integrations: {
auth: betterAuth({
instance: auth,
}),
},
});
```
This keeps working as the advanced extension path. Farm mounts the app-owned instance, while the application continues to use Better Auth's native server APIs and client. See the [Better Auth integration guide](/docs/integrations/auth/better-auth) for the complete setup.
Do not configure top-level `auth` and `integrations.auth` together. Each path owns the auth catch-all route, so Farm reports a configuration error instead of choosing one implicitly.
For a provider-owned UI, enterprise SSO, or another provider-specific API, choose one of the other [auth integrations](/docs/integrations/auth).
---
## Cache and PPR
URL: /docs/cache-ppr
Use shared runtime cache helpers, tag/path invalidation, ISR-style revalidation, and static shell caching for PPR pages.
# Cache and PPR
Use shared runtime cache helpers, tag/path invalidation, ISR-style revalidation, and static shell caching for PPR pages.
## Configure a shared cache
Farm uses its process-local memory cache when `cache.adapter` is not configured. For multiple
servers or ephemeral deployments, configure one shared adapter in `farm.config.ts`:
The memory cache retains at most 1,024 entries by default and evicts the least recently used entry
when it reaches that limit. Set `cache.maxEntries` to a larger positive integer when one process
must keep a larger working set:
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
cache: {
maxEntries: 4_096,
},
});
```
```bash
pnpm add @farm.js/cache-redis ioredis
```
```ts
import { defineConfig } from "@farm.js/core";
import { redisCache } from "@farm.js/cache-redis";
import Redis from "ioredis";
export default defineConfig({
cache: {
adapter: redisCache({
client: () => new Redis(process.env.REDIS_URL!),
}),
namespace: process.env.FARM_CACHE_NAMESPACE || "storefront",
},
});
```
The adapter is selected once. Routes, queries, endpoints, server functions, ISR, and PPR continue
using Farm cache keys and invalidation helpers; application handlers do not import a Redis client.
If framework configuration is reloaded or a test calls `configureFarmCache()` again, Farm starts a
new cache generation. A fill that began under the previous adapter or namespace can still return to
its original caller, but it cannot populate the newly configured cache.
When a shared adapter is present, Farm uses it as the authoritative cache instead of adding an
incoherent process-local front cache.
The `cache` option also carries the browser-side counterpart: `cache.client.adapter` points at a
client module that persists opt-in reads on the device. See
[persist the client cache](/docs/api-client#persist-the-client-cache); the two adapters configure
different caches on different machines and never share a store.
## Cache data
**server data**
```ts
import { createFarmCacheKey, getFarmDataCache } from "@farm.js/core/cache";
const cache = getFarmDataCache();
const key = createFarmCacheKey(["products", "featured"]);
const products = await cache.getOrSet(key, () => fetchProducts(), {
tags: ["products"],
paths: ["/pricing"],
revalidate: 300,
});
```
`revalidate` is measured in seconds and must be a finite, non-negative number. `0` makes an entry
immediately stale, while `false` explicitly disables time-based expiry. Invalid numeric values are
rejected instead of being treated as an indefinite cache lifetime.
`clear()` invalidates entries and any fills already in progress. Existing callers still receive
their result, but an older fill cannot repopulate the cache after it has been cleared.
## Memoize a function
`unstable_cache()` wraps an async function so its results are cached and shared across requests,
processes, and restarts.
```ts
import { unstable_cache } from "@farm.js/core/cache";
const getProduct = unstable_cache(async (id: string) => fetchProduct(id), ["product"], {
tags: ["products"],
revalidate: 300,
});
```
The cache key is built from the function's identity (its name and a hash of its source), the active
locale, the `keyParts` array, and the call arguments. Identity comes from the source rather than the
closure instance so the key stays stable across processes and restarts — which lets a shared adapter
reuse entries between server instances.
Because of that, **list every variable the function closes over that is not one of its arguments in
`keyParts`.** Two closures with identical source text but different captured values otherwise share a
cache entry and return each other's data:
```ts
// Collides: `table` is captured, not an argument, so it never reaches the key.
const makeLoader = (table: string) => unstable_cache(async (id: string) => db.get(table, id));
// Correct: the captured value disambiguates the two closures.
const makeLoader = (table: string) =>
unstable_cache(async (id: string) => db.get(table, id), [table]);
```
Values passed as call arguments already participate in the key, so they do not need to be repeated
in `keyParts`.
## Revalidate
**server action or route handler**
```ts
import { revalidatePath, revalidateTag } from "@farm.js/core/cache";
revalidateTag("products");
revalidatePath("/pricing");
```
`revalidatePath()` accepts a pathname or an HTTP(S) URL. Query strings and fragments do not change
the cache path and are removed during normalization.
### Invalidation counts under a shared adapter
The `count` reported on invalidation events (`cache.revalidateTag`, `cache.revalidatePath`) and the
`ppr.shell.invalidated` event are derived from Farm's process-local entry tracking. When a shared
`cache.adapter` is configured, entries and PPR shells live in the adapter rather than in local
memory, so these counts reflect only what the current process has tracked — they can read as `0`,
and `ppr.shell.invalidated` may not be emitted, even though the invalidation is still propagated to
the adapter and applied. Treat these counts and events as best-effort observability signals rather
than a confirmation of how many entries or shells were affected across a distributed deployment.
## PPR shell
Partial Prerendering is experimental and disabled by default. Enable it once in
`farm.config.ts`, then opt routes in individually. Without the flag, route-level PPR
declarations are ignored and those routes render fully dynamically.
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
experimental: {
ppr: true,
},
});
```
**src/app/dashboard/page.tsx**
```tsx
export const experimental_ppr = true;
export const revalidate = 60;
export default function DashboardPage() {
return Static shell with dynamic sections;
}
```
Farm's own `export const ppr = true` and a top-of-file `"use ppr"` (or `"use ppr; 60"`)
directive are equivalent opt-ins; `experimental_ppr` matches the Next.js export name.
`farm explain ` reports whether a route's PPR declaration is active or ignored.
When Farm serves a cached shell with dynamic holes, the browser refresh request bypasses browser and
shared HTTP caches. The completed response is private and is not stored, so the refresh cannot
receive the same cached shell again.
The client waits for a shell's background refresh before hydrating. Farm applies the completed
response's page props, route slots, manifests, locale, and deferred values first, so hydration sees
the same data that produced the refreshed HTML.
The background refresh reconciles the completed response into the existing shell. Stable elements
keep their DOM identity, focus, text selection, form values, and scroll position while server-owned
attributes and content update.
If the background refresh fails, hydration still proceeds after the first attempt. Farm retries a
failed refresh with backoff, at most three times and only while the browser is online, and stops as
soon as hydration starts, so a retry never replaces DOM the client has taken over.
## Cache keys and tags
Use stable keys for data and broad tags for invalidation. Keys identify one cached value, while tags let multiple values be refreshed together.
```ts
const key = createFarmCacheKey(["products", productId]);
const product = await cache.getOrSet(key, () => getProduct(productId), {
tags: ["products", `product:${productId}`],
paths: ["/pricing"],
revalidate: 300,
});
```
## Invalidate after writes
After a mutation, invalidate the route path and any data tags that feed the page.
For structured route data and [`createServerQuery`](/docs/server-queries) entries, call `invalidate(["resource", id])`. Farm uses the same route-data tag on the server and carries the invalidation to browser query and API consumers after a server action.
Server functions can declare the same relationship. Farm applies the targets only after the
handler succeeds and its output passes validation:
```ts
import { createServerFn } from "@farm.js/core/server-fn";
export const updateProduct = createServerFn({
input: UpdateProduct,
invalidates: ({ input, result }) => [
{ key: ["product", input.id] },
{ key: ["products", "list"] },
{ tag: "products" },
{ path: `/products/${result.id}` },
],
async handler({ input }) {
return db.product.update({
where: { id: input.id },
data: { name: input.name },
});
},
});
```
Endpoints accept the same `{ key }`, `{ tag }`, and `{ path }` targets. Imperative
`invalidate(...)`, `revalidateTag(...)`, and `revalidatePath(...)` remain supported. Await them
when calling outside a Farm action/endpoint request; Farm action requests automatically wait for
registered distributed invalidations before completing.
If a tag is invalidated while its value is still being generated, that caller can finish with the
generated value, but Farm keeps the stored entry stale so the next read regenerates it.
## Adapter contract
Custom adapters implement asynchronous entry persistence plus optional shared tag versions:
```ts
import type { FarmCacheAdapter } from "@farm.js/core/cache";
export const adapter: FarmCacheAdapter = {
async get(key) {
return backend.get(key);
},
async set(key, entry) {
await backend.set(key, entry);
},
async delete(key) {
await backend.delete(key);
},
async getTagVersions(tags) {
return backend.getTagVersions(tags);
},
async invalidateTags(tags) {
await backend.invalidateTags(tags);
},
};
```
`getTagVersions` and `invalidateTags` coordinate invalidation without scanning every cached entry.
Provider-specific adapters should use atomic increments or transactions when their backing service
supports them. Lease-capable adapters return an ownership token from `acquireLease` and only
release the lease when `releaseLease(key, token)` still matches that owner.
For any Farm/unstorage-compatible client, `storageCacheAdapter(storage)` provides a portable
baseline. `@farm.js/cache-redis` adds Redis-native atomic tag increments and regeneration leases.
**src/app/api/products/route.ts**
```ts
import { revalidatePath, revalidateTag } from "@farm.js/core/cache";
export async function POST(request: Request) {
const input = await request.json();
await updateProduct(input);
await revalidateTag("products");
await revalidatePath("/pricing");
return Response.json({ ok: true });
}
```
## PPR with Suspense holes
PPR works best when the stable page shell is outside Suspense and request-specific or slow data lives inside Suspense. As above, the route export only takes effect with `experimental.ppr` enabled in `farm.config.ts`.
When a production render actually suspends, Farm currently buffers that response for late status
errors and bypasses the shared PPR shell cache. This preserves fresh request-specific content
instead of caching a completed response as though it were a reusable partial shell.
```tsx
import { Suspense } from "react";
export const experimental_ppr = true;
export const revalidate = 60;
export default function BillingPage() {
return (
Billing
Loading subscription...}>
);
}
```
## Observability events
Cache and PPR emit events such as `cache.hit`, `cache.miss`, `cache.stale`, `cache.revalidatePath`, `cache.revalidateTag`, `ppr.shell.hit`, `ppr.shell.cached`, and `ppr.suspense.holeDetected`. Subscribe in `farm.config.ts` when debugging refresh behavior.
## Production notes
- Use one namespace per application or intentionally shared cache domain.
- Do not share authenticated values unless the cache key partitions by user or tenant.
- Treat adapter failures as production errors; declared mutations wait for invalidation.
- Use tags for data families, such as `products` or `billing`.
- Use paths for route-level invalidation, such as `/pricing`.
- Do not cache user-specific secrets in shared keys.
- Prefer PPR for pages with a mostly stable shell and a few dynamic sections.
---
## CLI
URL: /docs/cli
Use the Farm CLI to run, build, generate types, migrate apps, deploy output, and add integrations.
# CLI
Use the Farm CLI to run, build, generate types, migrate apps, deploy output, and add integrations.
## Create an app
```bash
pnpm create @farm.js/app my-app --template basic --typescript
pnpm create @farm.js/app my-preact-app --template basic --renderer preact --typescript
pnpm create @farm.js/app my-solid-app --template basic --renderer solid --typescript
pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescript
pnpm create @farm.js/app my-svelte-app --template basic --renderer svelte --typescript
pnpm create @farm.js/app stripe-app --template stripe --typescript
pnpm create @farm.js/app --list-templates
```
The create-app CLI includes Basic, Farm.js Auth, Better Auth, and one ready-to-configure starter
for every provider supported by `farm add integration --ui`. See [Getting Started](/docs/getting-started#choose-a-starter)
for the complete template catalog.
React is the default renderer. `--renderer preact`, `--renderer solid`, `--renderer vue`, and
`--renderer svelte` are available with the Basic starter; integration starters currently target React. See
[Renderers](/docs/renderers) for setup and feature compatibility.
## Common commands
| Command | Purpose |
| -------------------------------- | -------------------------------------------------------------------- |
| farm dev | Start the dev server. |
| farm build | Build the app for the configured target. |
| farm start | Run the Node server produced by farm build. |
| farm upgrade --latest | Upgrade installed Farm packages to the latest stable release. |
| farm upgrade --beta | Upgrade installed Farm packages to the latest beta release. |
| farm doctor | Inspect a running app, or fall back to project configuration checks. |
| farm doctor --offline | Check project files and config without probing a dev server. |
| farm doctor --registry | Verify the public Farm beta tags against the npm registry. |
| farm doctor --fix | Apply safe, additive project corrections. |
| farm explain /products/42 | Explain the files and runtime behavior for one URL. |
| farm preview | Create a public URL for a running local app. |
| farm generate | Generate route/API types and integration schema artifacts. |
| farm generate --check | Fail when committed generated types are stale. |
| farm deploy --plan | Resolve the deployment plan without executing it. |
| farm migrate inspect | Detect supported framework migration sources. |
| farm migrate next --write | Apply a deterministic Next.js App Router migration. |
| farm migrate tanstack --write | Apply a deterministic TanStack Router file-route migration. |
| farm migrate | Run one-shot schema or provider migration commands. |
| farm schema check | Check every declared table and reference against the live database. |
| farm schema migrate --apply | Create every plugin's tables, dependencies first. |
| farm add integration stripe --ui | Add integration wiring and optional UI. |
| farm cron list | List configured UTC schedules and target routes. |
| farm cron run dailyCleanup | Invoke one cron route on a running app. |
| farm dev --cron | Start the dev server with the opt-in in-memory cron scheduler. |
| farm telemetry status | Show the anonymous product-telemetry preference. |
| farm telemetry enable | Enable anonymous Farm.js product telemetry. |
| farm telemetry disable | Opt out and delete the local anonymous installation ID. |
See [Anonymous telemetry](/docs/telemetry) for the exact event fields, environment overrides, and
privacy guarantees. Telemetry is enabled by default for interactive local commands.
## Upgrade Farm packages
```bash
farm upgrade --latest
farm upgrade --beta
farm upgrade --latest --dry-run
```
`--latest` selects the newest stable release published under npm's `latest` tag. `--beta` selects
the newest prerelease published under the `beta` tag. Exactly one release channel is required.
Farm reads the app's dependencies and upgrades every published `@farm.js/*` package together. It
detects npm, pnpm, Yarn, or Bun from `packageManager` and lockfiles, preserves whether each package
is a regular, development, optional, or peer dependency (including packages repeated across sections), and skips local `workspace:`, `file:`,
`link:`, `portal:`, and `catalog:` references. Use `--dry-run` to inspect the commands without
changing the project.
## Provider names
The integration generator supports ai, stripe, supabase, workos, auth0, clerk, better-auth, authjs, autumn, polar, resend, jobs-trigger, jobs-inngest, and unkey.
## Add integrations
```bash
farm add integration stripe
farm add integration stripe --ui
farm add integration better-auth --ui
farm add integration jobs-trigger
```
Without `--ui`, the CLI installs the selected first-party provider package and its integration
wiring. With `--ui`, it also installs app-owned shadcn-style components for that provider. Farm
package versions follow the app's installed `@farm.js/core` version, including beta releases.
## Generate types
```bash
farm generate
farm generate --check
```
`farm dev` refreshes generated route/API types automatically when page and API route files change. Use `farm generate` when you want to refresh the same files outside the dev server, such as in CI, before publishing a package, or after moving files while the dev server was stopped.
When integration database schemas are configured, `farm generate` also writes schema artifacts if Farm can detect the data layer. Pass `--orm` and `--output` when you want explicit schema output for a migration step.
`farm generate --check` computes the route, API, environment, and i18n declarations without writing to disk. It exits non-zero and lists every missing, stale, or obsolete generated file. Use it after a normal generation step when generated types are committed:
```yaml
- run: farm generate --check
```
Schema output flags such as `--orm` and `--output` cannot be combined with `--check`; database schema generation can depend on migrations and remains an explicit write operation.
## Framework migrations
```bash
farm migrate inspect
farm migrate next
farm migrate next --write
farm migrate tanstack --write
```
Framework migrations are deterministic codemods. They inspect the project, print a dry-run plan by default, and only write when `--write` is passed. Existing target files are skipped unless `--force` is passed.
The Next.js migrator copies `app` or `src/app` into Farm's `src/app`, creates `farm.config.ts`, creates a minimal root layout when needed, rewrites supported imports from `next/link`, `next/navigation`, and `next/headers`, and updates package scripts to `farm dev`, `farm build`, and `node .output/server/index.mjs`.
The TanStack migrator converts file routes from `src/routes` or `routes` into `src/app/**/page.tsx`, maps `$id` to `[id]`, maps `$` to `[...splat]`, and adds a default export for simple `component: ComponentName` route files.
Both migrators leave source files in place and print manual review notes for framework-specific APIs.
## Run command migrations
```bash
farm migrate
farm migrate --dry-run
farm migrate --command "pnpm prisma migrate deploy"
```
`farm migrate` runs one-shot commands from `migrations.commands` in `farm.config.ts`. Use it for app-owned database migrations, integration tables, provider setup commands, and CI steps that should happen before `farm build`.
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
migrations: {
commands: [
{
name: "prisma",
command: "pnpm prisma migrate deploy",
},
{
name: "integration schema",
command: "farm generate --orm postgres --output ./schema/farm.sql",
},
],
},
});
```
Commands run sequentially and stop on the first failure. `--dry-run` prints the commands without executing them.
## Instant preview
```bash
farm preview
farm preview --port 3000
farm preview --name stripe-webhook
farm preview --url http://localhost:4319
farm preview --dry-run
```
`farm preview` exposes the app that is already running locally. It does not build or deploy the app. Use it when a teammate, webhook provider, OAuth provider, mobile device, or browser automation tool needs a public URL for the current `farm dev` session.
Farm detects the running app on common local ports, or you can pass `--port` / `--url` when the app is running somewhere specific. By default `@farm.js/tunnel` tries to open one outbound WebSocket to the hosted Farm Preview relay and returns a URL such as `https://stripe-webhook.preview.farmjs.dev`. During the relay rollout, the CLI falls back to compatibility gateway polling if the native connection is unavailable.
The preview terminal logs forwarded traffic:
```txt
GET /api/hello?name=something -> 200 621ms
POST /api/auth/login -> 200 580ms
```
The local `farm dev` terminal logs the matched page, API route, and middleware work. Client components hydrate through the same public URL; early button clicks are queued and replayed after hydration so slow dev-mode module loading does not lose the click.
See [Instant Preview](/docs/preview) for webhook setup, custom gateway configuration, troubleshooting, and security notes.
## Diagnose the app
```bash
farm doctor
farm doctor --port 4319
farm doctor --url http://localhost:4319
farm doctor --offline
farm doctor --offline --registry
farm doctor --fix
farm doctor --json
```
`farm doctor` first probes the running app at `http://localhost:3000`. A live app provides the most accurate result because Farm can report its resolved routes, API methods, middleware, integrations, storage mounts, schedules, workflows, layers, and deployment runtime. When no app is running, the command falls back to config and filesystem checks.
Pass `--port` or `--url` when the app runs somewhere else. An explicitly requested runtime that cannot be reached is reported as a warning before the project checks. Use `--offline` to skip the runtime probe. Add `--registry` to verify that the public `@farm.js/core` and `@farm.js/create-app` beta tags resolve and to warn when an exact Farm dependency is stale.
Project checks cover Node and pnpm support, package-manager and workspace resolution, generated type freshness, selected renderer and integration entrypoints, routing, deployment, storage, and schedules. The command is read-only by default: it does not install dependencies, run package-manager scripts, update generated files, or contact providers. Registry access is opt-in.
The command exits with a non-zero status only when a check fails. Warnings keep a zero exit status, so CI can distinguish broken configuration from production-readiness advice. `--json` prints the complete report without terminal formatting and preserves the same exit-code contract.
`farm doctor --fix` applies only corrections Farm can make without replacing application code. Today that means creating a missing `src/app/layout.tsx`; an existing file is never overwritten. The command reruns diagnostics after each correction and reports exactly which files it created.
See [Doctor](/docs/doctor) for diagnostics, the JSON contract, and CI examples, and [Built-in DevTools](/docs/devtools) for the browser dashboard.
## Explain a route
```bash
farm explain /products/42
farm explain /products/42 --json
```
`farm explain` resolves a URL against the app router without starting the app. It reports:
- the matching page file, route pattern, parameters, and source layer;
- inherited layouts and file- or config-based middleware;
- route-rule and module runtime controls, rendering mode, PPR, and cache settings;
- static or generated metadata and the nearest Open Graph and Twitter images;
- the deployment target, preset, runtime compatibility, and actionable warnings.
Request paths are not required to contain valid percent escapes. `farm explain` keeps a malformed
segment raw, matching the runtime router, instead of aborting the diagnostic with a URI error.
Use the text output while debugging and `--json` for tooling. The command is read-only and fails when no page route matches the supplied URL.
## Build
```bash
farm build
```
`farm build` respects `deploy.target`, `output`, and provider-specific output options from `farm.config.ts`.
## Plan a deployment
```bash
farm deploy --plan
farm deploy --vercel --prod --plan
```
`farm deploy --plan` resolves the target, Nitro preset, runtime, output directory, and exact build and provider commands. It does not build, inspect build output, check credentials, or call a deployment CLI. This makes it safe to use during review and in CI policy checks.
## Cron commands
```bash
farm cron list
farm cron list --json
farm cron run dailyCleanup
farm cron run dailyCleanup --url http://localhost:4319
farm dev --cron
```
`farm cron run` reads the named schedule from `farm.config.ts`, sends GET to its configured route, and forwards `CRON_SECRET` as bearer authorization. `farm dev --cron` runs the same routes on their UTC schedules in memory, prevents overlap inside the local process, and stops with the dev server.
See [Cron](/docs/cron) for configuration, production adapters, security, and reliability semantics.
## Production notes
- Run `farm generate` in CI if generated route/API types are not committed.
- Run `farm migrate` before `farm build` when schema changes are deployed separately from app code.
- Use `farm build` before deployment to catch docs, API, route, and integration wiring issues.
- Prefer `farm add integration --ui` only when you want generated UI files.
- Review generated integration files before committing them.
---
## Configuration
URL: /docs/configuration
Use farm.config.ts as the single project control plane for source paths, integrations, docs, KV storage, database clients, deployment, and framework behavior.
# Configuration
Use farm.config.ts as the single project control plane for source paths, integrations, docs, KV storage, database clients, deployment, and framework behavior.
## Define config
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
deploy: {
target: "vercel",
},
docs: {
entry: "/docs",
},
md: {
expose: ["/", "/pricing"],
cache: 60,
},
mdx: {
components: "./src/markdown-components.tsx",
},
theme: {
default: "system",
},
});
```
`srcDir` defaults to `"src"`. Set it only when the application source lives somewhere else.
`defineConfig` is the canonical Farm helper. `defineFarmConfig` remains available as a deprecated exact alias for existing applications.
## TypeScript
Farm transpiles TypeScript through the selected renderer and Vite. It reads the project's normal
`tsconfig.json`, but `farm build` does not run a separate project type check. Keep type checking as
an explicit script so the same command runs locally and in CI:
```json title="package.json"
{
"scripts": {
"type-check": "tsc --noEmit"
}
}
```
A top-level `typescript.tsconfigPath` or `typescript.ignoreBuildErrors` setting has no effect in
Farm. Configure compiler behavior in `tsconfig.json`; point an explicit `tsc -p` command at a
different file when needed.
## Renderer
React remains the default renderer, so existing applications and configurations do not need to
change. Select another renderer when you want to author the UI with that library while keeping
FARMJS routing and server features. See [Renderers](/docs/renderers) for the feature matrix and
dedicated [React](/docs/renderers/react), [Preact](/docs/renderers/preact),
[Solid](/docs/renderers/solid), [Vue](/docs/renderers/vue), and
[Svelte](/docs/renderers/svelte) guides.
### Preact
Install Preact and its FARMJS renderer adapter:
```bash
pnpm add @farm.js/preact preact
```
```ts
import { defineConfig } from "@farm.js/core";
import { preact } from "@farm.js/preact";
export default defineConfig({
renderer: preact(),
});
```
Preact routes use `.tsx` or `.jsx`. The adapter configures Preact JSX, Prefresh, React compatibility
aliases, server rendering and streaming, and browser hydration. See the
[Preact Renderer](/docs/renderers/preact) guide for typed server calls and compatibility boundaries.
Create a ready-to-run Preact application from the CLI:
```bash
pnpm create @farm.js/app my-preact-app --template basic --renderer preact --typescript
```
### Svelte
Install the Svelte adapter and runtime:
```bash
pnpm add @farm.js/svelte svelte
```
```ts
import { defineConfig } from "@farm.js/core";
import { svelte } from "@farm.js/svelte";
export default defineConfig({
renderer: svelte(),
});
```
Routes can then use Svelte 5 components directly:
```text
src/app/layout.svelte
src/app/page.svelte
src/app/products/[id]/page.svelte
```
See [Svelte Renderer](/docs/renderers/svelte) for module route exports, layout snippets, hydration,
typed server calls, and current compatibility boundaries.
Create a ready-to-run Svelte application from the CLI:
```bash
pnpm create @farm.js/app my-svelte-app --template basic --renderer svelte --typescript
```
### Vue
Install Vue and its FARMJS renderer adapter:
```bash
pnpm add @farm.js/vue vue
```
```ts
import { defineConfig } from "@farm.js/core";
import { vue } from "@farm.js/vue";
export default defineConfig({
renderer: vue(),
});
```
Routes can then use Vue Single-File Components directly:
```text
src/app/layout.vue
src/app/page.vue
src/app/products/[id]/page.vue
```
FARMJS compiles the SFCs with Vue's Vite plugin, renders them with `createSSRApp` and
`renderToString`, and hydrates interactive routes in the browser. Layout children are exposed through
Vue's default ``. See the [Vue server-rendering guide](https://vuejs.org/guide/scaling-up/ssr)
for Vue-specific SSR constraints.
See [Vue Renderer](/docs/renderers/vue) for SFC route exports, hydration, typed server calls, and
current compatibility boundaries.
Create a ready-to-run Vue application from the CLI:
```bash
pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescript
```
### Solid
Install the Solid adapter and runtime:
```bash
pnpm add @farm.js/solid solid-js
```
```ts
import { defineConfig } from "@farm.js/core";
import { solid } from "@farm.js/solid";
export default defineConfig({
renderer: solid(),
});
```
The renderer controls component compilation, server rendering, and browser hydration. FARMJS continues
to own routing, layouts, API routes, middleware, data access, observability, and deployment, so those
server features use the same APIs with every renderer. UI code uses the selected library's native
primitives—for example, Solid signals instead of React hooks.
See [Solid Renderer](/docs/renderers/solid) for route conventions, client boundaries, typed server
calls, and current compatibility boundaries.
Create a ready-to-run Solid application directly from the CLI:
```bash
pnpm create @farm.js/app my-solid-app --template basic --renderer solid --typescript
```
Omitting `renderer` selects React. The renderer option is currently available for the Basic starter;
integration starters continue to use React while their UI packages are migrated individually.
### Docs config
Configure the docs runtime directly in `farm.config.ts`:
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
docs: {
entry: "/docs",
metadata: {
description: "Product guides and API reference.",
},
nav: {
title: "Acme Docs",
},
search: {
provider: "simple",
enabled: true,
},
pageActions: {
copyMarkdown: {
enabled: true,
},
},
llmsTxt: true,
sitemap: true,
robots: true,
},
});
```
This single property enables human-readable pages, markdown mirrors, search metadata, and
agent-readable docs routes. A separate `docs.config.*` or `docs.json` file is optional and intended
only for large serializable configurations; inline values always take priority. See
[Docs Engine](/docs/docs-engine) for content layout, generated routes, and API overrides.
The same `search` option configures the search provider and the docs interface. When it is enabled,
Farm mounts the shared Omni React search from `@farming-labs/theme`. The sidebar control and
`Cmd+K` on macOS or `Ctrl+K` elsewhere open the same search interface used by the other Farming Labs
framework adapters. Set `search: false` or `search.enabled: false` to remove the control, client
mount, and shortcut together.
## Important options
| Option | Use it for |
| ------------- | -------------------------------------------------------------------------------------- |
| extends | Composing local or package Farm layers with project-first overrides. |
| srcDir | Changing the app source folder from the default src. |
| renderer | Selecting React (default) or an adapter such as Preact, Svelte, Vue, or Solid. |
| api | Configuring the public root used by Farm's typed browser API client. |
| integrations | Registering built-in or custom integrations. |
| auth | Enabling Farm's built-in email/password auth, sessions, helpers, and hooks. |
| mcp | Composing API routes and standalone tools in one authenticated MCP server. |
| agent | Opt-in agent readiness: llms.txt, schema.org JSON-LD, and AI crawler rules. |
| theme | Enabling light, dark, and system modes with client and server APIs. |
| storage | Configuring KV drivers/mounts and, in the current beta, an integration DB client. |
| migrations | Running one-shot schema/provider commands with `farm migrate`. |
| schema | Allowing plugins to add columns (`allowExtend`) and foreign keys (`allowForeignKeys`). |
| cron | Mapping portable UTC schedules to ordinary GET API routes. |
| i18n | Configuring locale routes, detection, message catalogs, typing, and direction. |
| docs | Serving the built-in docs runtime and docs API. |
| md | Restricting or disabling automatic markdown mirrors like /pricing.md. |
| mdx | Rendering `page.md` and `page.mdx` app routes, plus MDX components. |
| telemetry | Controlling automatic production-site reporting to Farm's usage dashboard. |
| deploy | Selecting a target, preset, and output directory. |
| deploymentId | Detecting stale browser requests during rolling deployments. |
| trailingSlash | Choosing the canonical URL shape for application page routes and links. |
| routeRules | Applying rendering, cache, redirect, CORS, and header behavior to route patterns. |
| security | Applying an app-wide CSP with an enforcing or report-only response header. |
| serverActions | Restricting trusted action origins and request body size. |
| images | Configuring responsive widths, remote allowlists, formats, and optimizer limits. |
| performance | Budgeting image and font preload hints without changing the rendered resources. |
| experimental | Auditing or enabling opt-in rendering experiments such as isolated hydration and PPR. |
| openapi | Publishing API reference docs. |
## Application base path
Set `basePath` when the complete application is mounted below the origin root. Farm applies the
canonical path consistently to routes, links, assets, and runtime endpoints:
```ts title="farm.config.ts"
export default defineConfig({
basePath: "/console",
});
```
Farm accepts a leading-slash or bare pathname and removes duplicate and trailing slashes. It rejects
URLs, query strings, hashes, backslashes, control characters, and `.` or `..` segments because a
browser could otherwise resolve a different path than Farm's server router.
## Trailing slashes
Application page URLs omit a trailing slash by default. Set `trailingSlash: true` to generate
framework links with a slash and redirect matching page requests to that canonical URL in both
development and production:
```ts title="farm.config.ts"
export default defineConfig({
trailingSlash: true,
});
```
The redirect uses status 308 and preserves the query string. The root URL remains `/`, and API,
integration, image, and metadata routes keep their own URL contracts. A ``
or `` prop overrides the app default for that link.
## API client base URL
Farm's typed API client uses the current origin and `/api` by default. Configure `api` when the
browser should call a different origin or path:
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
api: {
baseURL: ({ mode }) =>
process.env.GITHUH_API_URL ?? (mode === "development" ? "http://127.0.0.1:8080" : undefined),
basePath: "/api",
},
});
```
An origin-only `baseURL`, such as `https://api.example.com`, is joined with `basePath`. If
`baseURL` already contains a path, such as `https://api.example.com/v1`, that path is the API root
and `basePath` is ignored. Both fields accept a string or a sync/async resolver receiving
`{ root, mode, env }`. Farm resolves the function during configuration and only embeds the resulting
public URL in the browser bundle.
A root-relative API root is also mounted by Farm in development and production. For example,
`api: { basePath: "/v2/api" }` makes a route declared at `app/api/users/route.ts` available at
`/v2/api/users`. Farm treats an absolute `baseURL` as external and does not remount the current
application's API routes for it. `basePath` rejects backslashes, control characters, and `.` or
`..` segments, including encoded path separators. Root-relative `baseURL` and `basePath` values
must begin with one slash; network-path references such as `//api.example.com` are rejected so the
server mount and browser URL cannot resolve to different origins or paths.
The option configures the HTTP `apiClient` returned by `createApiClients()` automatically
(and the existing `createAPIClient()` factory). The paired server `api` always uses the local app's
registered routes and server mount, never an external browser API origin. An explicit per-client `baseURL` still
takes precedence.
This also applies to RSC builds and their Nitro servers, including apps configured with
`defineConfig` from `@farm.js/plugin/rsc`. API requests at the custom prefix stay on the API
pipeline rather than being decoded as server actions. The canonical `/api` routes remain available;
an external API URL changes the client destination only, not the local server mount.
## MCP transport
Install `@farm.js/mcp`, then configure one authenticated transport directly—no plugin array is
needed:
```ts title="farm.config.ts"
import { defineConfig } from "@farm.js/core";
export default defineConfig({
mcp: {
authorize: async ({ request }) => {
const session = await getSession(request);
return session ? { subject: session.user.id } : false;
},
},
});
```
This mounts `/api/mcp`. Typed API routes are not exposed as MCP tools unless their endpoint config sets
`mcp: true` or supplies MCP metadata, or you explicitly select endpoint instances in `mcp.tools`.
`authorize` receives `{ request, tool, tools, server }`; return `{ subject, tools: ["tool_name"] }`
to limit both discovery and invocation for that caller. You can also add standalone `defineTool()`
definitions from `@farm.js/mcp` to the same list; these validate their own input and receive the
authorized principal without creating separate HTTP routes. Optional `outputSchema` validates
standalone results; endpoint-backed tools reuse a route factory's `output` validator. Both advertise
the validated result shape to MCP clients. See [API MCP](/docs/plugins/mcp) for
mixed declarations, the resolved catalog, and permission checks.
## Agent readiness
`agent` holds opt-in features that help AI agents and crawlers understand a public site, and decide
which of them may use it. All of them are off by default, so internal tools and private dashboards
are unaffected.
```ts title="farm.config.ts"
import { defineConfig } from "@farm.js/core";
export default defineConfig({
agent: {
llmsTxt: {
title: "Acme",
summary: "Billing for small teams.",
exclude: ["/admin/[...path]"],
},
noindexPreviews: true,
jsonLd: true,
crawlers: { search: "allow", training: "block" },
},
});
```
`noindexPreviews: true` adds `X-Robots-Tag: noindex, nofollow` to every response from Farm's server
on a preview deployment; [Preview deployments](/docs/deployment#preview-deployments) covers how
Farm detects one.
`llmsTxt: true` serves [`/llms.txt`](https://llmstxt.org): a Markdown index of every static page,
with each page's metadata title and description, linking to its [Markdown mirror](/docs/markdown)
when one is exposed. An options object sets the `title` and `summary` (the root layout's metadata by
default), adds `details` Markdown, and narrows the list with `include` and `exclude` route patterns,
which use the same syntax as `md.expose`. Dynamic routes are left out because they have no single
URL.
It also serves `/llms-full.txt`: the same header, then a block per listed page with its title, URL,
and description, separated by `---`. Pages with an exposed Markdown mirror get it inlined; a page
without one keeps its block but no body. Pages are read with a fresh request that carries no cookies
or credentials, so a page behind auth is left out rather than copied into a file anyone can fetch.
`full: false` turns off the generated `/llms-full.txt` (a `public/llms-full.txt` or `llms-full.ts`
still serves that path). Rendering llms-full.txt renders every listed page, so set `revalidate`
(seconds) to let a CDN cache both generated files on busy sites.
Each file can be overridden on its own. A static `public/llms.txt` or `public/llms-full.txt` is
served as-is. An [`llms.ts` or `llms-full.ts` metadata route](/docs/routing#application-metadata-routes)
replaces the generated file with whatever it returns, either the complete file as a string or the
structured format, and receives the generated pages and defaults to build on. When the
[docs engine](/docs/docs-engine) is enabled too, the app's own files take `/llms.txt` and
`/llms-full.txt`.
`jsonLd: true` adds a schema.org `Organization` to page heads, built from the site's metadata: the
site name or title, `metadataBase`, and description. A page with none of those gets no JSON-LD. An
object sets the `type` (emitted as `@type`) and fields such as `name`, `url`, `logo`, and `sameAs`.
Pages and layouts add their own structured data with `metadata.jsonLd`, an object or an array of
objects. It works without `agent.jsonLd`; when both are set, the site script comes first.
```tsx title="src/app/blog/[slug]/page.tsx"
import type { MetadataProps } from "@farm.js/core";
export async function generateMetadata({ params }: MetadataProps<"/blog/[slug]">) {
const post = await getPost(params.slug);
return {
title: post.title,
jsonLd: {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
datePublished: post.publishedAt,
},
};
}
```
Each object renders its own `
```
Keep ordinary templates, `
{@render children?.()}
```
Keep ordinary Svelte markup, runes, stores, actions, transitions, and scoped styles. Replace
SvelteKit imports and conventions inside each component, and move FARMJS route exports such as
`metadata` and `hydrate` into `
```
Then define and forward the method used by application code:
```ts
export const track =
defineForward<(event: string, properties?: Record) => void>(
"mixpanel.track",
);
```
Use the initialization snippet recommended by the provider when it does more than a direct
`init()` call.
## Options
```ts
partytown({
forward: [track, "dataLayer.push"],
debug: false,
fallbackTimeout: 4_000,
strictProxyHas: true,
});
```
| Option | Default | Purpose |
| ----------------- | --------------------------------- | ---------------------------------------------------------------------- |
| `forward` | `[]` | Typed handles or safe dotted global paths to forward into the worker. |
| `debug` | Development `true`, build `false` | Select the readable Partytown debug runtime. |
| `fallbackTimeout` | Partytown default | Wait time before opted-in scripts fall back to the main thread. |
| `strictProxyHas` | Partytown default | Escape hatch for SDKs that depend on strict JavaScript `in` semantics. |
Set `fallbackTimeout: 0` when a main-thread fallback is less desirable than dropping the optional
third-party script.
There is no `enabled` option. Adding `partytown()` enables the plugin; removing it disables the
plugin and all of its runtime and build work.
## Preserve an existing function
Partytown normally replaces a forwarded main-thread function with its queue. If an existing
implementation must continue running, opt in while defining the bridge:
```ts
const push = defineForward<(event: Record) => number>("dataLayer.push", {
preserveBehavior: true,
});
```
The bridge still returns `void`. Worker calls cannot synchronously return a vendor result to browser
code.
## What Farm handles
- The bootstrap and worker files are served under `/~partytown/`, including a configured
`basePath`.
- Production assets land in Nitro's public directory, including the separate Vercel static output.
- Debug worker files are omitted from production unless `debug: true` is explicit.
- The bootstrap is an external same-origin script rather than plugin-owned inline JavaScript.
- Bootstrap injection is idempotent when an actual script carries `data-farm-partytown`; matching
text elsewhere in the document does not suppress it.
Partytown's worker scope can coexist with a root PWA service worker because the browser selects the
most specific matching scope.
## Compatibility and tradeoffs
Partytown proxies browser APIs, but not every third-party script is a good worker candidate. Keep
DOM-heavy widgets, consent interfaces, and APIs that require synchronous return values on the main
thread. Test each provider's important flows in a production build.
The plugin uses Farm's final HTML transform because Partytown's bootstrap must be present in the
document head before opted-in scripts run. Farm buffers that document when the plugin is enabled, so
this trades streamed HTML delivery for less third-party work on the browser main thread. Measure the
result for your application rather than assuming every page wins.
For Content Security Policy, allow the vendor's required script and network origins. Farm's own
bootstrap and workers stay same-origin. See Partytown's
[configuration](https://partytown.qwik.dev/configuration/) and
[tested services](https://partytown.qwik.dev/common-services/) when evaluating an SDK.
---
## PWA Plugin
URL: /docs/plugins/pwa
Generate a route-aware service worker with safe offline navigation, update prompts, and concise SWR image caching.
# PWA Plugin
`@farm.js/pwa` turns a production Farm build into an installable, offline-aware Progressive Web App.
It generates a service worker from the final client output, maps emitted static routes to their HTML
files, and registers the worker through Farm's browser lifecycle. Advanced applications can replace
the generated worker with a prebuilt JavaScript file while retaining that registration lifecycle.
## Install
```bash
pnpm add @farm.js/pwa
```
## Start with the short configuration
```ts title="farm.config.ts"
import { defineConfig } from "@farm.js/core";
import { pwa } from "@farm.js/pwa";
export default defineConfig({
plugins: [
pwa({
offline: "/offline",
cache: "auto",
}),
],
});
```
The generated worker always precaches immutable build assets. `cache: "auto"` additionally means:
- Precache every HTML page Farm emitted as a static route.
- Cache public same-origin images with SWR.
- Keep up to 100 image responses fresh for 30 days.
`auto` is the default, so you can omit `cache` for the same behavior. The previous `recommended`
value remains available as a compatibility alias. Service worker updates separately default to
`update: "prompt"`.
The generated worker only intercepts `GET` requests and same-origin URLs. Dynamic pages, APIs,
actions, integrations, and workflows remain network-owned.
## Add the offline page
The fallback has to be static because the worker must download it during installation.
```tsx title="src/app/offline/page.tsx"
export const ssg = true;
export default function OfflinePage() {
return (
You are offline
Reconnect to load fresh server data.
);
}
```
The production build fails with a clear message if `offline` does not resolve to emitted static
HTML. This prevents a worker from installing with a fallback that can never work.
## Add the application manifest
Use Farm's existing metadata route. Farm serves it at `/manifest.webmanifest`, so the plugin does
not ask you to repeat application metadata in PWA configuration.
```ts title="src/app/manifest.ts"
import type { MetadataRoute } from "@farm.js/core";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Acme Field App",
short_name: "Acme",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#16a34a",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}
```
## SWR in one line
Use the explicit form when you want individual cache controls instead of the automatic preset:
```ts
pwa({
cache: {
staticRoutes: true,
images: "swr",
},
});
```
`swr` means stale while revalidate:
1. The first request downloads and stores the image.
2. A later request returns the cached image immediately.
3. The worker fetches a newer response in the background.
4. The next request receives the refreshed image.
Cache Storage is shared by every account that signs in on the same browser profile and survives
sign-out, so the worker only stores an image it can safely show to anyone:
- The response is marked shareable with `Cache-Control: public` or a positive `max-age`, or has a
future `Expires`.
- Or it looks like a static file: no `Cache-Control` freshness, but a `Last-Modified` header. Static
files in `public/` are served this way.
An image is never stored when its request has an `Authorization` header, when its response is
marked `private`, `no-store`, `no-cache`, or `max-age=0`, or when it varies on `Cookie`,
`Authorization`, or `*`. A response with no caching headers at all is not stored either, which is
what an API route returning a per-account image usually sends. To have a dynamic image cached, send
an explicit policy such as `Cache-Control: public, max-age=3600`, and only for images that are the
same for every visitor.
Runtime image caching is best-effort. If browser storage is unavailable or full, a successful
network response still loads normally. If the network fails, an available cached image remains the
fallback; without one, the network error is preserved. Install-time precaching still requires all
selected assets to be cached successfully.
Customize the storage bounds with short names:
```ts
pwa({
cache: {
staticRoutes: ["/", "/help", "/pricing"],
images: {
strategy: "swr",
limit: 200,
ttl: "7d",
},
},
});
```
`limit` is the maximum image count. `ttl` accepts milliseconds or `s`, `m`, `h`, `d`, and `w`
durations such as `30s`, `5m`, `6h`, `30d`, or `2w`.
## Bring your own service worker
Use a custom worker when you need complete control over fetch routing, background sync, push
notifications, or a caching strategy outside the generated worker's scope:
```ts title="farm.config.ts"
pwa({
serviceWorker: {
source: "src/service-worker.js",
type: "module",
},
update: "prompt",
});
```
`source` is relative to the Farm project root. Farm copies it verbatim to the final `sw.js` path
under `basePath`; it does not bundle or transform the file. Module imports must therefore resolve
to files available in the production public output.
The custom worker owns install, fetch, offline, and cache behavior, so `serviceWorker` cannot be
combined with `offline` or `cache`. Farm still registers the worker and exposes its update events.
To support `applyUpdate`, handle the message used by Farm's update lifecycle:
```js title="src/service-worker.js"
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("message", (event) => {
if (event.data?.type === "FARM_PWA_SKIP_WAITING") void self.skipWaiting();
});
```
If you need to control registration and scope as well, leave out the PWA plugin and register the
worker directly in application client code.
## Update behavior
The default is `update: "prompt"`. When a new service worker finishes installing, the plugin
dispatches an event instead of reloading a page that may contain unsaved work:
```ts title="src/client.ts"
window.addEventListener("farm:pwa:update-available", (event) => {
const { applyUpdate } = (event as CustomEvent).detail;
showUpdateButton({ onClick: applyUpdate });
});
```
Application code can also call:
```ts
window.__FARM_PWA__?.applyUpdate();
```
Set `update: "auto"` only when reloading cannot discard user input. Auto mode activates a waiting
worker and reloads once the new worker controls the page.
## Options
Adding `pwa()` to `plugins` enables service worker generation and registration. Remove it from the
array when the app should not ship a service worker.
| Option | Default | Description |
| --------------- | ---------- | ------------------------------------------------------------------- |
| `offline` | `false` | Static fallback route for the generated worker. |
| `update` | `"prompt"` | Prompt or automatically activate and reload for a waiting worker. |
| `cache` | `"auto"` | Generated-worker caching, a custom object, or build assets only. |
| `serviceWorker` | `false` | Prebuilt worker source and optional `"classic"` or `"module"` type. |
| Cache option | Default under `auto` | Description |
| -------------- | -------------------- | ---------------------------------------------------- |
| `staticRoutes` | `true` | Every emitted static page, a route list, or `false`. |
| `images` | `"swr"` | SWR options, `true`, `"swr"`, or `false`. |
`offline` and explicit `staticRoutes` entries must be stable root-relative application paths.
URLs, query strings, fragments, dot segments, backslashes, control characters, and encoded path
separators are rejected.
Write these paths without the deployment prefix: with `basePath: "/app"`, `"/"` is served at
`/app` and `"/help"` at `/app/help`. An application route named `"/app"` is distinct from the home
page and is cached at `/app/app`. Farm handles localized HTML output automatically; include the
locale in a localized route such as `offline: "/en/offline"`, but do not add the deployment prefix.
## Production lifecycle
During `farm build`, generated-worker mode:
1. Finds the preset's final public output.
2. Hashes precached files and caching options into a deployment-specific cache ID.
3. Writes `sw.js` under Farm's configured `basePath`.
4. Maps clean static route URLs to emitted HTML files.
5. Fails if the configured offline page is missing.
Generated cache names are isolated by `basePath`, so multiple Farm applications on one origin do
not delete or reuse a sibling application's precache or image cache.
Custom-worker mode copies the configured source to the same deployment-aware `sw.js` location and
leaves its contents untouched.
In the browser, the plugin registers only for production builds. Service workers require HTTPS in
production; browsers also permit localhost for development and local production testing.
See the complete runnable source in
[`examples/pwa-demo`](https://github.com/farming-labs/farm.js/tree/main/examples/pwa-demo).
---
## Plugin Tables
URL: /docs/plugins/schema-tables
Let a plugin declare the tables its schema needs, so farm migrate can create them from the schema alone.
# Plugin Tables
A plugin that takes a `schema` usually needs somewhere to put the rows. Farm can
create those tables from the schema itself, so neither you nor the people using
your plugin have to transcribe it into SQL by hand.
Declare the tables once and every app gets a command:
```bash
pnpm farm migrate
```
## Using it
If a plugin you have configured declares tables, the command prints the SQL its
schema implies and stops:
```bash
pnpm farm sync migrate
```
```sql
-- Generated by `farm sync migrate` from the sync schema.
-- Dialect: sqlite
-- Review before applying. Farm never alters existing tables.
CREATE TABLE IF NOT EXISTS "tasks" (
"id" TEXT PRIMARY KEY,
"title" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'open',
"listId" TEXT NOT NULL,
"updatedAt" TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS "tasks_listId_idx" ON "tasks" ("listId");
```
Read it, then run it:
| Flag | What it does |
| ---------------- | ------------------------------------------------------------- |
| _(none)_ | print the statements |
| `--write ` | save them to a file to commit alongside your other migrations |
| `--apply` | execute them |
`migrate` reports what is _missing from your database_, so it needs to reach it
even to print. For a full dump that needs no connection, use
`farm generate --orm postgres|mysql|sqlite` — it reads the same declarations and
emits the same SQL, plus Prisma and Drizzle schemas.
The plugin name is the one the plugin declares, not its package name. Ask an app
what it has by naming one that does not exist:
```
$ pnpm farm jobs migrate
No plugin named "jobs" owns tables in this app. Available: sync.
```
### It adds, and never changes or drops
When a plugin's tables already exist, `migrate` adds what a table that may hold
rows can safely take, and reports the rest:
- **Added:** missing columns that are nullable or have a default (existing rows
get the default), and missing plain indexes.
- **Reported, never applied:** a required column without a default, a unique
column or index (existing rows could break it), a changed column, and
anything the database has that the schema does not.
```
[info] These tables exist but no longer match the schema. Farm will not change them:
tasks
missing in the database: slug
not in the schema: legacy_note
changed column status: default expected "open", found none
```
A create or an addition is derivable from the schema alone. A change is not: a
rename and a drop-plus-add look identical from here, and one of them destroys
data. That call stays with the person who knows which one it was, so take it to
your own migration tooling.
Statements are written so `--apply` is safe to re-run.
### Upgrading a plugin
Farm remembers what it last applied for each plugin, in a `farm_schema_state`
table in your database, with the plugin's `version`. When you upgrade the plugin
and run `migrate`, it shows what that version changed before touching anything:
```
$ pnpm farm teams migrate
teams 1.0.2 → 1.1.0 changes its tables:
+ invitation new table
+ member.role string
+ member_role_idx index
Apply these changes to teams's tables now? (y/N)
```
In a terminal it asks; in scripts and CI it prints the plan and stops, and
`--apply` applies it. Changes a release makes that Farm will not apply on its
own, such as a removed or redefined column, are listed with `~`.
`farm schema check` also warns while an upgrade is waiting:
```
! teams (plugin, postgres)
warning teams upgraded 1.0.2 → 1.1.0, and 3 change(s) are not applied yet.
```
The dev server says so too, once it is up:
```
⚠️ teams 1.0.2 → 1.1.0 has 3 change(s) to its tables that are not applied yet. Run `farm teams migrate` to review them.
```
It only reads, runs in the background after startup, and stays quiet when a
database is unreachable, so it never slows `farm dev` down. Apps whose plugins
own no tables never run it.
Set `version` in `definePlugin` so the summary can name the versions. Without
it, Farm says the tables changed since they were last migrated. In a project
with Prisma or Drizzle, no `farm_schema_state` table is created: their own
migrations track history there, and `farm generate --orm` carries the plugin's
new tables and columns into their schema.
### When it does not apply
Two setups need nothing, and say so rather than guessing:
- **A storage mount.** Key-value storage has no tables. The command reports
`Nothing to create` and exits cleanly.
- **A schema owned by Prisma, Drizzle, or another ORM.** Their migrations are
the source of truth. Keep using them, and wire them into Farm so they run with
everything else:
```ts title="farm.config.ts"
export default defineConfig({
migrations: { commands: ["pnpm prisma migrate deploy"] },
});
```
## Migrating every plugin at once
`farm schema migrate` runs every plugin's migration in one go, dependencies
first. A plugin whose tables point at another plugin's (`billing` at `teams`)
waits for it:
```bash
pnpm farm schema migrate # print every plan, in order
pnpm farm schema migrate --apply # create the tables
pnpm farm schema migrate --write migrations/farm.sql
```
```
Migrating in order: teams → billing → audit
```
It behaves like running `farm migrate` for each plugin in that order,
with the same flags, approvals, and Prisma or Drizzle handling. If one plugin
fails, the plugins that depend on it are skipped, the rest still run, and the
command exits with an error listing what did not finish. Running it again only
does what is left.
Your own ORM's or library's migrations are not run by it. Run those first, with
[`farm migrate`](/docs/cli#run-command-migrations) or their own command.
## Checking everything at once
`migrate` looks at one plugin. `farm schema check` looks at every integration
and plugin that declares tables, against the database each one uses, and at the
tables they point to that Farm does not manage, such as your ORM's `users` or the
tables Better Auth creates:
```bash
pnpm farm schema check
```
```
✗ loyalty (plugin, postgres)
error Table "points" does not exist.
Run `farm loyalty migrate` to see the SQL, then `--apply` it.
error "points.userId" references "users.id", but "users" does not exist.
Farm does not manage that table. Run the migration that creates it (your ORM's, or a library's such as Better Auth) first.
2 error(s), 0 warning(s)
```
Errors are things that break at runtime:
- a table, or a column, the owner needs is missing, or a column's type changed
- a referenced table or column does not exist. When the table belongs to another
plugin, the message names it so you know which `migrate` to run first, and it
is looked up in that plugin's database
- two owners claim the same table in the same database
- an owner's database cannot be reached. The other owners are still checked
Warnings do not fail the check:
- other drift, such as defaults, indexes, and foreign keys
- a reference whose column types differ, such as text pointing at a Postgres
`uuid`. Lookups by value work, but a SQL join needs a cast and a foreign key is
not possible. SQLite compares across types, so it is not flagged there
- a client Farm cannot inspect, such as a Prisma client. That ORM's migrations
own those tables. Drizzle databases are read through the driver they wrap
A referenced table is found the way your app's queries find it: through the
connection's search path on Postgres, or as `schema.table` when the reference is
dotted, such as `auth.users` in Supabase. Tables a plugin creates are looked up
in the current schema, because that is where `migrate` creates them.
Connecting, and each query, has 10 seconds to answer, so an unreachable
database fails the check instead of hanging it. Pass `--timeout ` to change
it.
It only reads. Nothing is created or altered, so it is safe to run against
production. It exits `1` when there are errors, which makes it a deploy gate:
```yaml
- run: pnpm farm schema check
```
Pass `--json` for the report as data. Owners that store data in a key-value
mount have no tables and are listed as skipped.
## Declaring tables from a plugin
Give `definePlugin` the schema:
```ts title="src/index.ts"
import { definePlugin, defineSchema } from "@farm.js/core";
export const teamsSchema = defineSchema({
models: {
organization: {
fields: { id: { type: "uuid", primaryKey: true }, name: { type: "string" } },
},
member: {
fields: {
id: { type: "uuid", primaryKey: true },
organizationId: {
type: "uuid",
reference: { model: "organization", field: "id", onDelete: "cascade" },
},
},
},
},
});
export function teams() {
return definePlugin({ name: "farm:teams", schema: teamsSchema });
}
```
That is the whole contract. Apps that configure the plugin get:
- `farm teams migrate`: the command is the part of the plugin name after its
last `:` or `/`
- the plugin's models in `farm generate --orm prisma|drizzle|...`, alongside
every integration's
- its tables in `farm schema check`
The tables live in the app's database, `storage.client` in `farm.config.ts`,
the same one integrations use. A broken schema fails when the config loads.
### Tables from another plugin
A reference to a table another plugin creates, or columns added to one, makes
that plugin a dependency: `farm schema check` lists it first and shows what each
plugin needs, as in `billing (plugin, postgres, needs teams)`. Add
[`dependsOn`](/docs/plugins/create-plugin#depend-on-another-plugin) as well, so
an app that installs your plugin without the other one fails while its config
loads instead of at the first query.
### Foreign keys between plugins
A reference to another plugin's table becomes a real foreign key when that is
safe: the other table is in the same database, the referenced column is its
primary key or unique, and the column types match. It is created with the
table, so `farm schema migrate` gets it for you by creating the other plugin's
tables first:
```sql
CREATE TABLE IF NOT EXISTS "subscription" (
"id" TEXT PRIMARY KEY,
"organizationId" TEXT NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE
);
```
If the other plugin has not created its table yet, the table is created without
the key and the command says to run `farm schema migrate`. A table that already
exists never gets a foreign key on its own: it may hold rows that point at
nothing. Allow it, and Farm counts those rows first and adds the key only when
there are none:
```ts title="farm.config.ts"
schema: {
allowForeignKeys: {
billing: ["subscription"];
}
}
```
SQLite can only add a foreign key when it creates a table. References to the
app's own tables, and references with `enforced: "app"` or `"none"`, never get
one.
### Tables the plugin uses but does not create
A plugin often points at a table the app owns, such as the `user` table its auth
library creates. Describe it with `external: true`:
```ts
user: {
external: true,
fields: { id: { type: "string", primaryKey: true } },
},
member: {
fields: {
// ...
userId: { type: "string", reference: { model: "user", field: "id" } },
},
},
```
Farm never creates an external table: `migrate`, generated Prisma and Drizzle
schemas, and conflict checks all skip it, and no foreign key points at it. It is
described so the reference resolves to the real table, `farm schema check` can
verify that table exists, and the plugin can query it.
### Letting the app use its own names
The app's tables may not be called what your schema calls them. Accept renames
as an option and apply them with `renameSchema`:
```ts title="src/index.ts"
import { definePlugin, renameSchema, type FarmSchemaRenames } from "@farm.js/core";
type TeamsOptions = { schema?: FarmSchemaRenames };
export function teams(options: TeamsOptions = {}) {
return definePlugin({
name: "farm:teams",
schema: renameSchema(teamsSchema, options.schema),
});
}
```
```ts title="farm.config.ts"
teams({
schema: {
user: { name: "members_auth", fields: { id: "user_id" } },
member: { name: "team_members" },
},
});
```
Only names change. Migrate creates `team_members`, the check looks for
`members_auth.user_id`, and at runtime the plugin keeps its own names: an ORM
built from the renamed schema with `createIntegrationOrm` reads `user.id` from
`members_auth.user_id`. Renames are typed from the plugin's schema, so the app
gets completion, and a model or field that does not exist fails when the config
loads.
### Adding columns to the app's tables
Sometimes a plugin needs data on a row the app owns, such as points on each
user. Describe the table as external and `extend` it with the columns:
```ts title="src/index.ts"
export const loyaltySchema = defineSchema({
models: {
user: { external: true, fields: { id: { type: "string", primaryKey: true } } },
pointsHistory: {
fields: {
/* ... */
},
},
},
extend: {
user: { fields: { points: { type: "integer", default: 0 } } },
},
});
```
The app's table may already hold rows, so each column must have a `default` or
be `nullable`. Unique, indexed, and primary-key columns cannot be added this
way. Both fail when the config loads.
Nothing changes in the app's table until the app allows it:
```ts title="farm.config.ts"
export default defineConfig({
plugins: [loyalty()],
schema: { allowExtend: { loyalty: ["user"] } },
});
```
List the plugin's model names or your own table names. Without the entry,
`farm loyalty migrate` prints the `ALTER TABLE` commented out with the entry to
add, `--apply` creates the plugin's own tables but not the columns and exits
with an error, and `farm schema check` reports each missing column. With it,
`--apply` adds them:
```sql
ALTER TABLE "user" ADD COLUMN "points" INTEGER NOT NULL DEFAULT 0;
```
Farm only adds. A column that already exists is never changed, even with a
different type: the check warns instead. If the table does not exist yet, the
command says so and still creates the plugin's own tables.
**When Prisma or Drizzle owns the table**, a column added behind its back is
drift its next migration would undo. In a project with `prisma/schema.prisma`
or a `drizzle.config.*`, Farm never alters the app's tables. `migrate` and
`farm generate --orm` print the line to add to your schema instead:
```
// In the Prisma model mapped to "user":
points Int @default(0)
```
### When the app picks another database
Where data lives is the app's decision. By default a plugin's tables go in the
app's `storage.client`, and the plugin sets nothing. Some plugins let the app
choose a different database for them through their own options, the way Sync
takes `sync({ client })`. Forward that choice with `database`:
```ts title="src/index.ts"
export function jobs(options: { client?: unknown } = {}) {
return definePlugin({
name: "farm:jobs",
schema: jobsSchema,
// Undefined when the app passed nothing: storage.client is used.
database: { client: options.client },
});
}
```
`client` can also be a function that returns the connection. Farm only calls it
from tooling (`migrate`, `generate`, `schema check`), never on the request path.
It detects what it is given:
| Connection | Result |
| ----------------------------------------------------------------- | ----------------------------------------------------------- |
| `pg`, `mysql2`, `node:sqlite`, or anything with `query`/`prepare` | migrated |
| an unstorage mount, or anything with `getItem`/`setItem` | reported as having no tables |
| anything else | an error naming your plugin and pointing at its own tooling |
Set `database.dialect` when a connection exposes a generic `query` and is not
Postgres: the shape alone cannot tell Postgres and MySQL apart.
### Only create what you own
Every model in `schema` is a table the plugin creates, except the ones marked
`external: true`. When an app hands your plugin a schema it shares with the rest
of its code, mark the models the plugin was not given control of as external. A
model the plugin was never given is not its table to create.
## What is generated
Everything comes from schema metadata, so the SQL is derivable and reviewable
before it runs:
| Declaration | What it emits |
| -------------------------------- | ------------------------------------------------------------------ |
| `name` on a model or field | the real table or column name |
| `type` | the column type for the target dialect |
| `primaryKey: true` | `PRIMARY KEY` |
| `unique: true` | `UNIQUE` on the column |
| `index: true` | `CREATE INDEX "
__idx"` |
| `constraints: [...]` | `CREATE [UNIQUE] INDEX "
__"` |
| `default` | a literal, for strings, numbers, and booleans |
| `default: "now"` on a `datetime` | `DEFAULT CURRENT_TIMESTAMP` |
| `reference` | Relationship metadata; database enforcement is the default |
| `reference.enforced: "db"` | A SQL/Drizzle foreign key or Prisma relation, including `onDelete` |
| `reference.enforced: "app"` | Keeps relationship metadata without a database foreign key |
| `reference.enforced: "none"` | Keeps the declaration without a database foreign key |
Postgres, SQLite, and MySQL are supported. Prisma and Drizzle output preserves
database-enforced internal references instead of reducing them to comments. A
`reference` to a model outside this owner's schema is left as a comment rather
than a foreign key, since the other table may not exist yet. Omitting `enforced`
is equivalent to `enforced: "db"`. Application and unenforced references stay
available in schema metadata, but SQL/ORM generation and the runtime ORM do not
create a database constraint for them.
Database-enforced internal references are dependency ordered so parent tables
are created before their dependents, and a table can reference itself. Farm
rejects cross-table cycles of database-enforced references before emitting SQL
because inline foreign keys cannot create either table first; mark one edge
`enforced: "app"` or manage that constraint separately when a schema needs that
shape.
### Two things worth knowing
**Columns are `NOT NULL` by default.** A field is nullable only when it says so:
```ts
updatedAt: { type: "datetime" } // NOT NULL
updatedAt: { type: "datetime", nullable: true } // nullable
updatedAt: { type: "datetime", required: false } // nullable
```
**Names fall back to the schema's own keys**, which is exactly what the runtime
reads, so the tables this creates are the tables your queries find. A model
`tasks` with a field `listId` becomes `tasks("listId")`, not `tasks("list_id")`.
Set `name` on a model or field to point at a different one.
**Primary keys follow `primaryKey`, not the field type.** `id` and `uuid` fields
without `primaryKey: true` remain ordinary string fields. Farm supports primary
keys on `id`, `uuid`, `string`, and `integer` fields; other field types fail
during schema resolution before SQL or ORM artifacts are generated.
---
## Scripts
URL: /docs/plugins/scripts
Load external browser SDKs with typed handles, deliberate timing, consent gates, dependencies, retries, and Farm lifecycle integration.
# Scripts
`@farm.js/scripts` manages browser SDKs that arrive through external `
{@render children?.()}
```
Page route props such as `params` and `searchParams` are available through `$props()` when the
component needs them.
## Hydrate interactive routes
Export `hydrate = true` from the module script, then use Svelte runes and events normally:
```svelte
```
FARMJS server-renders the component with Svelte's server runtime and claims the existing markup with
Svelte hydration in the browser.
## Call FARMJS server code
API routes, endpoint schemas, server functions, middleware, cache, storage, and observability are
renderer-neutral. Use the generated typed API client from a Svelte component:
```svelte
```
The endpoint can call a validated `createServerFn`; its handler, database access, and secrets remain
in the server bundle.
## Current boundaries
Use the Svelte-native bindings for router, action, server-query, theme, and i18n stores:
```svelte
```
The returned values implement Svelte's readable-store contract. Renderer-specific `Link` and form
components, fetchers, integration providers, programmatic UI routes, Markdown/MDX visual pages,
the docs adapter, and generated JSX metadata images remain React-oriented today.
The Better Auth starter includes native Svelte routes, runes, and forms:
```bash
pnpm create @farm.js/app my-auth-app --template better-auth --renderer svelte --typescript
```
Other integration starter templates currently target React, so add their renderer-neutral provider
code to a native Basic starter.
Run the complete example:
```bash
pnpm --filter farm-svelte-renderer-example dev
```
See the [Svelte renderer example](https://github.com/farming-labs/farm.js/tree/main/examples/svelte-renderer),
the [renderer support matrix](/docs/renderers), and Svelte's
[SSR guide](https://svelte.dev/docs/svelte/svelte-server).
---
## Vue Renderer
URL: /docs/renderers/vue
Use Vue Single-File Components, SSR, hydration, and typed FARMJS server calls through the @farm.js/vue adapter.
# Vue Renderer
`@farm.js/vue` connects Vue Single-File Component compilation, `createSSRApp` rendering, and browser
hydration to the FARMJS renderer contract. React remains the default unless the application selects
Vue.
## Create an app
```bash
pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescript
```
For an existing Basic app, install the adapter and Vue runtime:
```bash
pnpm add @farm.js/vue vue
```
```ts
import { defineConfig } from "@farm.js/core";
import { vue } from "@farm.js/vue";
export default defineConfig({
renderer: vue(),
});
```
## Pages and layouts
Vue routes use `.vue` files:
```text
src/app/layout.vue
src/app/page.vue
src/app/products/[id]/page.vue
```
Use a normal script for FARMJS route exports and `
```
`inheritAttrs: false` prevents FARMJS route props such as `path` from falling through to the root
DOM element. Declare the props with `defineProps` when the page needs them.
## Hydrate interactive routes
Export `hydrate = true` from the route's normal script, then use Vue state and events normally:
```vue
```
FARMJS server-renders the SFC with `createSSRApp` and Vue's native Node or WHATWG Web stream, then
uses `createSSRApp` again to claim the existing browser markup.
## Call FARMJS server code
API routes, endpoint schemas, server functions, middleware, cache, storage, and observability are
renderer-neutral. Use the generated typed API client from the SFC:
```vue
```
The endpoint can call a validated `createServerFn`; its handler, database access, and secrets remain
in the server bundle.
## Current boundaries
Use the Vue-native composables for reactive router, action, server-query, theme, and i18n state:
```ts
import { useAction, useRouter, useTheme } from "@farm.js/vue/bindings";
const router = useRouter();
const save = useAction(saveProduct);
const theme = useTheme();
await save({ name: "FARMJS" });
await router.push("/products");
theme.toggleTheme();
```
The composables return Vue refs and computed values. Renderer-specific `Link` and form components,
fetchers, integration providers, programmatic UI routes, Markdown/MDX visual pages, the docs
adapter, and generated JSX metadata images remain React-oriented today.
The Better Auth starter includes native Vue SFC routes, composables, and forms:
```bash
pnpm create @farm.js/app my-auth-app --template better-auth --renderer vue --typescript
```
Other integration starter templates currently target React, so add their renderer-neutral provider
code to a native Basic starter.
Run the complete example:
```bash
pnpm --filter farm-vue-renderer-example dev
```
See the [Vue renderer example](https://github.com/farming-labs/farm.js/tree/main/examples/vue-renderer),
the [renderer support matrix](/docs/renderers), and Vue's
[SSR guide](https://vuejs.org/guide/scaling-up/ssr).
---
## Route Runtime
URL: /docs/route-runtime
Choose where dynamic pages and API routes execute, inherit deployment defaults from layouts, and set provider duration and region hints.
# Route Runtime
Choose the execution runtime, regions, and maximum duration for dynamic pages and API routes. Farm uses one contract across file routes, programmatic routes, layouts, and `routeRules`.
## File pages
Export the controls next to the page that owns them. No wrapper or additional route file is required.
**src/app/reports/page.tsx**
```tsx
export const runtime = "node";
export const regions = ["iad1", "fra1"];
export const maxDuration = 30;
export default async function ReportsPage() {
const reports = await getReports();
return ;
}
```
`maxDuration` is measured in seconds. Region identifiers belong to the selected deployment provider; Farm validates the portable shape and lets the adapter interpret the identifiers.
## API routes
API files use the same exports.
**src/app/api/reports/route.ts**
```ts
export const runtime = "node";
export const regions = ["fra1"];
export const maxDuration = 15;
export async function GET() {
return Response.json(await getReports());
}
```
Layouts do not wrap API handlers, so API routes inherit matching `routeRules`, then apply their own named exports.
## Layout defaults
A layout sets defaults for pages below its segment. The complete layout tree executes in the runtime selected for the final page.
**src/app/admin/layout.tsx**
```tsx
import type { LayoutProps } from "@farm.js/core";
export const runtime = "node";
export const regions = ["fra1"];
export const maxDuration = 60;
export default function AdminLayout({ children }: LayoutProps) {
return {children};
}
```
A child can override only the value it needs:
**src/app/admin/analytics/page.tsx**
```tsx
export const maxDuration = 20;
export default function AnalyticsPage() {
return ;
}
```
Farm resolves controls from lowest to highest precedence:
1. Broad matching `routeRules`
2. More-specific matching `routeRules`
3. Parent layouts
4. Nearest layout
5. `page.tsx` or API `route.ts`
## Reset inherited values
Use `"auto"` when a route should return to the deployment provider's default instead of inheriting a layout or rule.
```tsx
export const runtime = "auto";
export const regions = "auto";
export const maxDuration = "auto";
```
An omitted export inherits. An explicit `"auto"` resets.
## Programmatic routes
The same fields are typed on every programmatic primitive.
**src/farm.routes.tsx**
```tsx
import { defineRoutes } from "@farm.js/core";
import { ReportsPage } from "./features/reports/page";
export default defineRoutes(({ page, layout, api }) => [
page("/reports", {
runtime: "node",
regions: ["iad1", "fra1"],
maxDuration: 30,
component: ReportsPage,
}),
layout("/admin", {
runtime: "node",
maxDuration: 60,
component: AdminLayout,
}),
api("/api/reports", {
runtime: "node",
regions: ["fra1"],
maxDuration: 15,
GET: async () => Response.json(await getReports()),
}),
]);
```
## Route rules
Use route rules when deployment policy belongs to a URL group rather than one source file.
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
routeRules: {
"/api/public/**": {
runtime: "node",
regions: ["iad1"],
maxDuration: 10,
},
"/admin/**": {
runtime: "node",
regions: ["fra1"],
maxDuration: 60,
},
},
});
```
Page, API, and layout values override matching rules. Rules still apply to generated integration endpoints that do not have a page file.
## Deployment behavior
Farm writes the normalized result to `.farm/route-runtime-manifest.json` on production builds. Custom deployment adapters can consume this manifest instead of parsing source files.
| Preset family | Runtime accepted | `regions` | `maxDuration` |
| ------------------ | ---------------- | ------------------------------- | ------------------------------- |
| `vercel` | `auto`, `node` | Enforced per generated function | Enforced per generated function |
| Cloudflare presets | `auto`, `edge` | Preserved for adapters | Preserved for adapters |
| `netlify` | `auto`, `node` | Preserved for adapters | Preserved for adapters |
| `netlify-edge` | `auto`, `edge` | Preserved for adapters | Preserved for adapters |
| `node-server` | `auto`, `node` | Preserved in the manifest | Preserved in the manifest |
| Custom preset | Adapter-defined | Preserved in the manifest | Preserved in the manifest |
For Vercel Node builds, Farm creates one Build Output API function per distinct region/duration policy and routes matching requests to it. Routes using provider defaults continue using the base function.
A production build fails when a dynamic route requests an incompatible runtime, such as `runtime = "edge"` with the Node-based `vercel` preset or `runtime = "node"` with a Cloudflare Worker preset. Farm warns when a valid runtime target cannot map a region or duration hint, while keeping the value available in the manifest.
Farm's current Vercel adapter emits Node functions. Use a supported Edge preset such as `cloudflare-module` or `netlify-edge` for `runtime = "edge"`; do not treat a Vercel region identifier as a portable Edge guarantee.
## Static routes and development
Static pages are generated during the build and do not have a request-time function. Runtime, region, and duration controls therefore do not change how a fully static page is served.
The local development server runs in Node so one process can provide HMR and route discovery. Runtime compatibility and provider output are enforced by `farm build`, which is the command CI should run before deployment.
## Practical guidance
- Keep functions close to their database, not necessarily close to every browser.
- Set `maxDuration` high enough for normal streaming responses but low enough to cap runaway work.
- Use `edge` only when the complete page, layout tree, middleware, and dependencies support Web APIs without Node-only modules.
- Prefer layout defaults for coherent application areas and route rules for generated or cross-cutting endpoints.
- Avoid configuring static pages unless the same policy also applies when the page becomes dynamic later.
---
## Routing
URL: /docs/routing
Farm uses an app directory routing model with static routes, dynamic segments, catch-all routes, and typed navigation.
# Routing
Farm uses an app directory routing model with static routes, dynamic segments, catch-all routes, and typed navigation.
The examples on this page use the default React renderer. Solid uses `.tsx`/`.jsx` route files, Vue
uses `.vue`, and Svelte uses `.svelte`. The route tree and server contracts stay the same; see
[Renderers](/docs/renderers) for component conventions and feature compatibility.
## File routes
| File | URL |
| ------------------------------- | ------------- |
| src/app/page.tsx | / |
| src/app/about/page.tsx | /about |
| src/app/about/page.mdx | /about |
| src/app/blog/[slug]/page.tsx | /blog/:slug |
| src/app/docs/[...slug]/page.tsx | /docs/:slug\* |
With Vue, the same routes use names such as `src/app/page.vue` and
`src/app/blog/[slug]/page.vue`.
With Svelte, use `src/app/page.svelte` and `src/app/blog/[slug]/page.svelte`.
## Programmatic routes
File routes cover most apps. Define routes in code instead when a URL can't be a folder, or when a
page and the endpoints it needs belong together. Export `defineRoutes` from `src/routes.ts` (or
`routes.tsx`, `farm.routes.ts`, `farm.routes.tsx`, and their `.js`/`.jsx` forms):
**src/routes.tsx**
```tsx
import { createEndpoint, defineRoutes } from "@farm.js/core";
import { z } from "zod";
import { ChangelogPage } from "./features/changelog/page";
import { listReleases, renderReleasesFeed } from "./features/changelog/releases";
export default defineRoutes(({ page, api }) => [
page("/changelog", { component: ChangelogPage }),
api("/changelog/feed.xml", {
GET: async () =>
new Response(renderReleasesFeed(await listReleases()), {
headers: { "content-type": "application/rss+xml" },
}),
}),
api("/changelog/releases.json", {
GET: createEndpoint(
{
method: "GET",
query: z.object({ limit: z.coerce.number().int().positive().default(20) }),
},
async ({ query }) => Response.json(await listReleases(query.limit)),
),
}),
]);
```
- `api()` takes the same handlers a `route.ts` exports, one per method (`GET`, `HEAD`, `QUERY`,
`POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`): a plain `(request) => Response` or a
`createEndpoint(...)` with request validation.
- Paths don't need the `/api` prefix that file API routes live under, and they can end in a file
extension such as `/feed.xml` or `/data.json`.
- `page()` and `layout()` take a component plus the route options covered below: data, guards,
search params, pending, error and not-found UI, and [route runtime](/docs/route-runtime) settings.
- Programmatic pages join the same route table as file routes, so generated route types,
`PageProps` and typed links include them. Programmatic API routes are not in the generated API
client types yet; use a file route under `src/app/api` when you need a typed client.
Prefer a file route when the URL maps to a folder. Reach for `defineRoutes` for colocated
features, URLs a folder can't express, and route lists generated from data or configuration.
## Named slots and intercepted routes
An `@name` directory gives its owning layout another rendered node alongside `children`. Use slots
for independently composed areas such as activity panels, drawers, and modals. The slot name and
interception marker never become part of the public URL.
```text
src/app/feed/
├── layout.tsx
├── page.tsx
├── photo/[id]/page.tsx
└── @modal/
├── default.tsx
└── (.)photo/[id]/page.tsx
```
The layout receives the `@modal` result as `modal`:
```tsx
import type { ReactNode } from "react";
export default function FeedLayout({
children,
modal,
}: {
children: ReactNode;
modal?: ReactNode;
}) {
return (
{children}
{modal}
);
}
```
`default.tsx` renders when no page in that slot matches. A client-side navigation from `/feed` to
`/feed/photo/42` selects `@modal/(.)photo/[id]/page.tsx`, places it in the existing modal slot, and
keeps the surrounding feed state alive. A direct request or refresh at `/feed/photo/42` renders the
canonical `photo/[id]/page.tsx` instead.
Interception markers are relative to the slot's owning route:
| Marker | Target base |
| ---------- | ---------------------- |
| `(.)` | Same route level |
| `(..)` | One route level above |
| `(..)(..)` | Two route levels above |
| `(...)` | App root |
Make an intercepting slot a Client Component when it needs event handlers such as closing a modal
with `router.back()`. Farm falls back to canonical document navigation if it cannot safely mount the
intercepted slot in the current page.
## Dynamic params
**src/app/users/[id]/page.tsx**
```tsx
import type { PageProps } from "@farm.js/core";
export default function UserPage({ params }: PageProps<"/users/[id]">) {
return
User: {params.id}
;
}
```
The route literal is checked against the generated application routes and narrows `params` to
`{ id: string }`. The same route-aware generic is available on `LayoutProps`, `LoadingProps`,
`ErrorProps`, `MetadataProps`, and `LayoutMetadataProps`. Omitting the generic keeps the existing
`Record` behavior for gradual adoption.
Use `GenerateStaticParams` to check build-time paths against the same route:
```tsx
import type { GenerateStaticParams } from "@farm.js/core";
export const generateStaticParams: GenerateStaticParams<"/users/[id]"> = async () => [
{ id: "ada" },
{ id: "grace" },
];
```
For an optional catch-all such as `/docs/[[...slug]]`, omit `slug` or return `slug: []` to
generate the parent `/docs` page. Farm exposes that empty capture as `params.slug === ""`, the
same value produced by request-time routing. JavaScript callers that return `{ slug: undefined }`
or `{ slug: null }` are normalized to the same empty value; typed callers should prefer omission
or an empty array.
## Typed navigation
Farm writes the route union into the consolidated `src/farm.d.ts` declaration file. Link hrefs and route component props accept real routes without widening everything to plain string. Link hrefs can also include query strings and hash fragments.
Changing only the fragment preserves SPA state, honors push versus replace history, and does not
request route data again.
Relative navigation, prefetch, and shallow history URLs resolve against the current document in
development and production. For example, `?tab=settings` from `/users/123?tab=profile` keeps
`/users/123`, and `456` from `/users/123` resolves to `/users/456`.
Native anchor behavior still takes precedence: for example, a `Link` with a `download` attribute is
handled by the browser instead of Farm's SPA router. Absolute URI schemes such as `mailto:`, `tel:`,
`sms:`, and same-origin `blob:` URLs are passed through unchanged and are never prefetched as app routes. Literal custom
schemes such as `customapp:open` are validated from their URI grammar and work without registration.
Viewport prefetch uses a short scroll guard and is cancelled if its link unmounts before the guard
expires. Intent prefetches are deduplicated while active; after an attempt settles, a later hover,
focus, or touch can retry while successful route data remains deduplicated by the router cache.
The development page-data router reuses a successfully decoded JSON prefetch response when
navigation overlaps it with the same URL, interception origin, and active layout chain.
Refreshes and different contexts fetch independently, and failed prefetches are retried by
navigation. Cancelling a waiting
navigation does not cancel its background prefetch. Deferred streaming responses keep independent
requests and cancellation ownership; a waiting navigation starts its own request once the
prefetch response headers identify a deferred stream.
Production HTML navigation also reuses matching in-flight prefetches. Different interception
contexts and deployment identities stay separate; failed prefetches are retried by navigation.
Refresh bypasses pending prefetches, and clearing the cache prevents their late responses from
repopulating it. Cancelling navigation does not abort a shared background prefetch.
When a production HTML request redirects, Farm hands the original URL to a full document
navigation so the browser preserves the redirect destination, fragments, and push/replace history.
Prefetching a redirect never navigates the page or caches destination HTML under the original URL.
Internal `Link` hrefs stay app-relative: when `basePath: "/console"` is configured, `href="/about"`
renders and navigates to `/console/about`. Do not add the base path to route hrefs yourself.
For a reusable custom-scheme type, use ``ExternalHref<`customapp:${string}`>`` (or declaration-merge
`LinkExternalUriSchemes` when the scheme should belong to the default `ExternalHref` union).
**Client navigation**
```tsx
import { Link } from "@farm.js/core/client";
export function Nav() {
return (
<>
About
Routing
>
);
}
```
## Lightweight router helpers
Use the lightweight router when client components, layouts, breadcrumbs, tabs, or tests need to match app routes without adding a separate routing library.
**src/lib/router.ts**
```ts
import { createFarmRouter } from "@farm.js/core/router";
export const router = createFarmRouter(["/", "/dashboard", "/users/[id]", "/docs/[[...slug]]"]);
```
```ts
const match = router.match("/users/ada?tab=settings");
if (match) {
console.log(match.route.path); // /users/[id]
console.log(match.params.id); // ada
}
```
Build hrefs from the same route patterns:
```ts
const href = router.build("/docs/[[...slug]]", {
slug: ["core", "routing"],
});
```
This returns `/docs/core/routing`. Optional catch-all params can be omitted, static routes win over dynamic routes, and route groups such as `(marketing)` do not appear in the URL.
Programmatic route patterns must remain stable when a browser parses their URLs. Farm rejects
dot-only segments, encoded path separators or control characters, and raw backslashes instead of
building a URL that the browser would canonicalize to a different route. Ordinary encoded static
segments such as `/files/hello%20farm` remain supported.
These matching rules also apply with experimental React Server Components enabled. Farm prepares page precedence when the route entry initializes, so filesystem discovery order cannot make `/users/[id]` hide `/users/new`. Optional catch-all pages match both their parent URL and deeper paths.
Route matching decodes each URL path segment once before comparing static names or exposing
`params`. Route names remain literal (including `%`), and malformed percent escapes remain literal
instead of aborting the request.
A required or optional catch-all must be the final URL segment. Farm reports paths such as
`docs/[...slug]/edit/page.tsx` during route discovery because the catch-all would otherwise consume
the `edit` segment and make the route unreachable.
Each dynamic segment in one route must have a unique parameter name. Farm rejects paths such as
`teams/[id]/members/[id]/page.tsx` instead of silently replacing the outer `id` value.
The prototype-sensitive names `__proto__`, `constructor`, and `prototype` are reserved because
JavaScript cannot represent them safely in the plain `params` object.
For navigation state, `router.isActive(pattern, pathname, { exact: false })` also matches
descendants after dynamic segments, such as `/users/42/settings` for `/users/[id]`.
Supplied path parameters must contain non-empty segments; omit an optional catch-all instead of
passing an empty string.
Client components can pass the same route list to `useRouter` when they want current route params:
```tsx
import { useRouter } from "@farm.js/core/client";
export function CurrentUserTab() {
const router = useRouter({
routes: ["/users/[id]", "/users/[id]/settings"],
});
return {router.params.id};
}
```
Imperative `push()` and `replace()` calls use SPA navigation for same-origin routes. Absolute
cross-origin URLs use normal document navigation instead of Farm's local page-data endpoint.
Internal router paths use the configured application `basePath`, matching `Link`; already-prefixed
paths are preserved.
## Navigation blocking
Use `useBlocker` when a client component needs to protect unsaved work before SPA navigation continues.
```tsx
"use client";
import { useBlocker } from "@farm.js/core/client";
export function ProductForm({ isDirty }: { isDirty: boolean }) {
useBlocker({
when: isDirty,
message: "You have unsaved changes.",
});
return ;
}
```
Blockers apply to Farm SPA navigation and browser unload prompts. They improve UX, but they do not replace server-side validation or persistence checks.
## Page state
Use page state for shallow UI state that belongs in browser history but should not reload route data: modals, drawers, selected panels, or temporary filters.
```tsx
"use client";
import { usePageState, useRouter } from "@farm.js/core/client";
export function ProductToolbar() {
const router = useRouter();
const page = usePageState<{ modal?: "cart"; drawer?: "filters" }>();
return (
<>
{page?.modal === "cart" ? : null}
>
);
}
```
Page state is stored in `history.state`, so back/forward navigation restores the previous state without changing the URL unless you pass an href.
## Scroll restoration
Farm restores window scroll during SPA navigation. Register nested scroll areas when a layout owns its own scroll container.
Locations with different query strings keep independent positions, so browser history restores the
correct filtered, searched, or paginated view.
```tsx
"use client";
import { useScrollRestoration } from "@farm.js/core/client";
export function DocsSidebar() {
const ref = useScrollRestoration("docs-sidebar");
return
{/* links */}
;
}
```
Use stable keys per scroll container. If two elements share a key, the latest mounted element owns that stored position.
## Route data cache
Programmatic routes can cache the value returned from `data.main`. This is useful for product pages, docs pages, dashboards, and other route data that should be reused during server rendering or prefetching.
The first `createRoute` argument is a pathname pattern only; declare typed search parameters with
`search` and add query strings or hashes when building a navigation URL.
```tsx
import { createRoute, invalidate } from "@farm.js/core";
import { z } from "zod";
import { ProductPage } from "./page";
export const ProductRoute = createRoute("/products/[id]", {
params: z.object({ id: z.string() }),
data: {
key: ({ params }) => ["product", params.id],
staleTime: "30s",
async main({ params }) {
return {
product: await db.product.findUnique({ where: { id: params.id } }),
};
},
},
component: ProductPage,
});
export async function saveProduct(id: string, name: string) {
await db.product.update({ where: { id }, data: { name } });
await invalidate(["product", id]);
}
```
`key` enables caching. When a cached entry is still fresh, Farm reuses the previous `data.main` result. `before` still runs for each request, and `after` still runs with the returned data, so setup and logging hooks keep their normal behavior.
`staleTime` accepts a number of milliseconds or a duration string such as `"500ms"`, `"30s"`, `"5m"`, or `"1h"`. Omit `staleTime` when data should stay cached until invalidated.
Farm also tags route data by the rendered path, so `revalidatePath("/products/123")` invalidates the matching route data entry. Use `tags` or `paths` when one mutation should refresh more than one route:
```tsx
export const ProductRoute = createRoute("/products/[id]", {
data: {
key: ({ params }) => ["product", params.id],
tags: ({ params }) => [`product:${params.id}`, "products"],
paths: ({ params }) => [`/products/${params.id}`, "/products"],
async main({ params }) {
return { product: await getProduct(params.id) };
},
},
component: ProductPage,
});
```
Cache keys are part of your data security model. If data depends on the current user, role, tenant, locale, or draft mode, include that value in `key` or avoid caching that route. Route cache invalidation improves freshness, but API routes and server functions still need their own authorization checks.
## Route actions
An action is a typed server function, not necessarily a URL route. Use a standalone
`createServerFn` when an operation is shared by several pages, belongs to a background job or
integration, or does not have one resource owner:
```ts
import { createServerFn } from "@farm.js/core/server-fn";
import { z } from "zod";
export const sendInvite = createServerFn({
input: z.object({ email: z.string().email() }),
async handler({ input }) {
return { queued: true, email: input.email };
},
});
```
```tsx
import { useAction } from "@farm.js/core/client";
import { sendInvite as sendInviteFn } from "./actions";
const sendInvite = useAction(sendInviteFn);
await sendInvite({ email: "ada@example.com" });
```
Attach actions to a route when they are commands on that route's resource. This gives the page a
typed namespace for related mutations, lets `useAction(route)` select a declared default, and lets
Farm expose an action-only client proxy without shipping the route loader, component, or database
code to the browser. The action still uses Farm's server-function transport; it does not become a
public URL such as `/products/update`.
With React Server Components and Server Actions enabled, a programmatic route can own named server
functions. Keep the functions in a dedicated server module so Farm can replace the route with an
action-only proxy when a Client Component imports it. The proxy contains no loader, component, or
database code.
**src/features/products/actions.ts**
```ts
import { createServerFn } from "@farm.js/core/server-fn";
import { z } from "zod";
import { db } from "./db";
export const updateProduct = createServerFn({
input: z.object({
id: z.string(),
name: z.string().min(2),
}),
invalidates: ({ input }) => [{ key: ["product", input.id] }],
async handler({ input }) {
return db.product.update({
where: { id: input.id },
data: { name: input.name },
});
},
});
export const publishProduct = createServerFn({
input: z.object({ id: z.string() }),
async handler({ input }) {
return db.product.update({
where: { id: input.id },
data: { published: true },
});
},
});
```
Attach the imported functions to the route. `defaultAction` selects the function returned by
`route.action` and used by `useAction(route)`. When the route has one action, or no explicit default,
Farm selects the first declared action.
**src/features/products/product.route.tsx**
```tsx
import { createRoute } from "@farm.js/core/routes";
import { ProductPage } from "./product-page";
import { publishProduct, updateProduct } from "./actions";
import { db } from "./db";
export const ProductRoute = createRoute("/products/[id]", {
data: {
key: ({ params }) => ["product", params.id],
async main({ params }) {
return { product: await db.product.findUniqueOrThrow({ where: { id: params.id } }) };
},
},
actions: {
update: updateProduct,
publish: publishProduct,
},
defaultAction: "update",
component: ProductPage,
});
```
Server code calls either the default or a named action as a normal typed function. Direct calls run
in process, while input/output validation, middleware, declared errors, and invalidation keep their
server-function behavior.
```ts
await ProductRoute.action({ id: "p1", name: "Keyboard" });
await ProductRoute.actions.publish({ id: "p1" });
```
In a Client Component, `useAction(ProductRoute)` wraps the default function in a callable RPC and
adds React state. Call the wrapper itself; no `.submit()` method is required.
```tsx
"use client";
import { useAction } from "@farm.js/core/client";
import { ProductRoute } from "./product.route";
export function RenameProduct({ id }: { id: string }) {
const update = useAction(ProductRoute);
return (
);
}
```
The wrapper exposes `pending`, `status`, `data`, `error`, `reset`, `formAction`, and `Form`. It keeps
the action's input, result, and declared error types. Choose another named action explicitly when
needed:
```tsx
const publish = useAction(ProductRoute.actions.publish);
const result = await publish({ id: "p1" });
```
For both `useAction` and `useServerFn`, `pending` means at least one submission since the last
reset is still running. `status`, `result` / `data`, and `error` describe the latest submission.
If that submission finishes before an older one, its status becomes `success` or `error` while
`pending` remains `true`. Finishing older work only reduces the pending count; it cannot replace
the latest result, error, or status. A new submission sets status back to `pending`, and `reset()`
returns it to `idle`.
The same wrapper supports progressive forms. The form performs a native server action before
hydration and uses the tracked RPC lifecycle after hydration:
```tsx
const update = useAction(ProductRoute);
return (
);
```
Route action entries must be imported identifiers such as `{ update }` or
`{ update: updateProduct }`. Do not create them inline inside `createRoute`; the separate module is
the server boundary Farm uses to generate safe browser references. When provided, `defaultAction`
must be a string literal matching one of those entries.
`useAction` keeps `Form`'s component identity stable across renders, including when you pass
inline `optimistic` callbacks or change the target action. Existing fields retain their DOM
identity, unsaved input, focus, and selection. Submissions use the latest action and options;
React's normal form reset after a successful function action still applies.
## Deferred route data
Use `defer()` for secondary data that should not delay the route shell. Farm returns the value from `data.main` as soon as its directly awaited work finishes, then streams explicitly deferred fields into nested React Suspense boundaries.
**src/features/products/page.tsx**
```tsx
import { Suspense, use } from "react";
import type { Deferred } from "@farm.js/core";
export function ProductPage({ data }: ProductPageProps) {
return (
{data.product.name}
}>
);
}
function Reviews({ reviews }: { reviews: Deferred }) {
const resolvedReviews = use(reviews);
return resolvedReviews.map((review) => );
}
```
**src/farm.routes.tsx**
```tsx
import { createRoute, defer } from "@farm.js/core";
import { ProductPage } from "./features/products/page";
export const ProductRoute = createRoute("/products/[id]", {
data: {
async main({ params }) {
const reviews = defer(getProductReviews(params.id));
const product = await getProduct(params.id);
return {
product,
reviews,
};
},
},
component: ProductPage,
});
```
In this example, both requests start together, but only `getProduct` controls when the route shell is ready. `getProductReviews` does not block the shell. The component receives `reviews` as `Deferred`, so React `use()` resolves it with full type inference.
`data.after` runs after `main` returns and receives the deferred promise without waiting for it. This keeps logging and request cleanup hooks from extending the stream; await a deferred field inside `after` only when that delay is intentional.
Farm uses a streaming page-data response for SPA navigation and serializes settled deferred values for hydration. Rejections expose a generic `DeferredDataError` to the browser while the original error remains in server logs. Deferred values and their resolved results must be JSON-serializable when they cross into a hydrated component.
Use `defer()` for independent secondary sections such as reviews, recommendations, activity, or analytics. Keep data required for the title, authorization decision, redirect, or primary above-the-fold content directly awaited in `main`. `defer()` is explicit: ordinary nested promises are not automatically treated as streamed route data.
## Typed Search Params
Programmatic routes can validate URL search params and define cleanup rules in one place. Use `search.schema` for typed route input, `stripDefaults` for clean URLs, `preserve` for params that should carry across links, and `temporary` for one-time UI params.
```tsx
import { createRoute } from "@farm.js/core";
import { z } from "zod";
export const ProductRoute = createRoute("/products/[id]", {
search: {
schema: z.object({
tab: z.enum(["info", "reviews"]).default("info"),
locale: z.string().default("en"),
toast: z.string().optional(),
}),
stripDefaults: true,
preserve: ["locale"],
temporary: ["toast"],
},
data: {
async main({ params, search }) {
return {
product: await getProduct(params.id),
tab: search.tab,
};
},
},
component: ProductPage,
});
```
With that route, `/products/123?tab=info&locale=am&toast=saved` gives the component typed search data:
```ts
{
tab: "info",
locale: "am",
toast: "saved",
}
```
After the route has consumed it, Farm can clean the URL to `/products/123?locale=am`. `tab=info` is removed because it matches the schema default, and `toast=saved` is removed because it is temporary.
`preserve` is used by Farm links. If the current page is `/products?locale=am`, then a link to `/products/[id]` carries `locale=am` unless the link already provides its own `locale`.
Use this for tab state, pagination defaults, locale/tenant preservation, preview mode, and one-time params such as `toast=saved`. Keep security-sensitive values out of search params; they are user-editable URL state, not trusted server state.
## Route context
Use `context` in `farm.config.ts` for request-scoped dependencies that guards and route data need: sessions, tenants, feature flags, or database clients. The value is available to programmatic route `guard`, `data.before`, `data.main`, cache key functions, and `data.after`.
```ts
import { defineConfig } from "@farm.js/core";
import { db } from "./src/db";
import { getSession } from "./src/session";
export default defineConfig({
context: async ({ request }) => ({
session: await getSession(request),
db,
}),
});
```
Add a module augmentation when you want autocomplete in route files:
```ts
import type { db } from "./src/db";
import type { getSession } from "./src/session";
declare module "@farm.js/core" {
interface FarmAppContext {
session: Awaited>;
db: typeof db;
}
}
```
Route context is server-only. Farm passes it to guards and data hooks without serializing it into browser props, so keep raw database clients and secrets in `context` and return only safe page data from `data.main`.
## Route guards
Use `guard` when a route should be allowed or blocked before route data loads. Guards run after params/search validation and before `data.before` or `data.main`.
```tsx
import { createRoute, redirect } from "@farm.js/core";
export const DashboardRoute = createRoute("/dashboard", {
guard: async ({ context }) => {
if (!context.session.user) {
redirect("/login");
}
},
data: {
async main({ context }) {
return { stats: await getDashboardStats(context.db) };
},
},
component: DashboardPage,
});
```
Use `guard` for route flow: auth redirects, role gates, tenant checks, and early `notFound()` decisions. Use `data.before` when you want to prepare values that `data.main` needs. A guard is not a complete authorization boundary; repeat sensitive authorization inside API routes and server functions because those can be requested directly.
## Route UI states
Programmatic routes can define local `pending`, `error`, and `notFound` components. `pending` is used as the Suspense fallback while route data resolves. `error` handles guard/data errors. `notFound` handles `notFound()` thrown from guard or data hooks.
```tsx
import { notFound } from "@farm.js/core";
export const ProductRoute = createRoute("/products/[id]", {
data: {
async main({ params }) {
const product = await getProduct(params.id);
if (!product) notFound();
return { product };
},
},
pending: ProductSkeleton,
error: ProductError,
notFound: ProductNotFound,
component: ProductPage,
});
```
Redirects are not rendered through `error`; they escape so Farm can return a real redirect response.
## Nested segments
Folders become URL segments. Use normal folders for visible path segments and dynamic folders when the value comes from the URL.
**Route tree**
```txt
src/app/
page.tsx
dashboard/
page.tsx
settings/
page.tsx
blog/
[slug]/
page.tsx
docs/
[...slug]/
page.tsx
```
This creates `/`, `/dashboard`, `/dashboard/settings`, `/blog/:slug`, and `/docs/:slug*`.
## Markdown pages
Use `page.md` or `page.mdx` for static content routes. They behave like app pages, participate in layouts, and get route types.
Markdown/MDX visual routes currently use the React content renderer. Preact treats this as a
`preact/compat` surface, while Solid, Vue, and Svelte applications can still serve renderer-neutral
API content or ordinary static assets but should use renderer-owned component pages for visual routes.
**src/app/about/page.mdx**
```mdx
# About
This page renders at `/about` and exposes source at `/about.md`.
```
When `page.tsx` and `page.md` or `page.mdx` share a folder, the React file owns the HTML route and
the markdown file becomes its exact `.md` representation. Without a sidecar, Farm derives markdown
from the rendered React page automatically.
## Metadata And OG Images
Export `metadata` for static head tags or `generateMetadata` when the values depend on route params, search params, middleware data, or route data. Farm merges layout metadata from root to leaf, then applies the page metadata last.
During HTML-based client navigation, Farm reconciles the destination document's title, meta tags,
canonical and alternate links, icons, manifest link, and
[page JSON-LD](/docs/configuration#agent-readiness). Tags omitted by the destination are removed,
so metadata from the previous route cannot remain active.
A layout can define a default title and a `%s` template for child segments. The layout itself uses
the default; a child string title is substituted into the nearest parent template:
```tsx
export const metadata = {
title: {
default: "Acme",
template: "%s | Acme",
},
};
```
**src/app/products/[id]/page.tsx**
```tsx
import type { MetadataProps } from "@farm.js/core";
export const metadata = {
description: "Product details",
openGraph: {
siteName: "Acme",
type: "website",
},
};
export async function generateMetadata({ params }: MetadataProps<"/products/[id]">) {
const product = await getProduct(params.id);
return {
title: product.name,
openGraph: {
title: product.name,
description: product.summary,
},
twitter: {
card: "summary_large_image",
title: product.name,
},
};
}
export default function ProductPage() {
return Product;
}
```
Use `generateMetadata` in a layout when the same dynamic metadata flow should apply to every page in a route subtree. Farm passes the matched route params to each layout and merges the result before applying the page metadata. A catch-all docs layout can therefore load the current document for every nested docs URL:
**src/app/docs/[...slug]/layout.tsx**
```tsx
import type { LayoutProps } from "@farm.js/core";
export async function generateMetadata({ params }: Pick) {
const slug = params.slug?.split("/") ?? [];
const document = await getDocument(slug);
return {
title: document.title,
description: document.description,
openGraph: {
title: document.title,
description: document.description,
type: "article",
},
twitter: {
card: "summary_large_image",
title: document.title,
description: document.description,
},
};
}
export default function DocsLayout({ children }: LayoutProps) {
return children;
}
```
Pair this layout with `opengraph-image.tsx` in the same `[...slug]` segment to generate a different PNG for each document. `generateMetadata` supplies the title, description, and social fields; the image file renders the PNG described below. Leave `openGraph.images` and `twitter.images` unset when Farm should attach the nearest generated image automatically. An explicit image value still takes precedence.
### Article fields
When `openGraph.type` is `"article"`, Farm also renders the Open Graph article properties:
```tsx
import type { MetadataProps } from "@farm.js/core";
export async function generateMetadata({ params }: MetadataProps<"/blog/[slug]">) {
const post = await getPost(params.slug);
return {
openGraph: {
type: "article",
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: ["https://acme.test/team/ada"],
section: "Engineering",
tags: post.tags,
},
};
}
```
`publishedTime` and `modifiedTime` take an ISO string or a `Date` and render
`article:published_time` and `article:modified_time`. `authors` and `tags` take a string or an
array and render one `article:author` or `article:tag` per item. `section` renders
`article:section`. Other `openGraph.type` values ignore these fields.
### Favicons
Place favicon files in `public/`, then declare them through the root layout metadata. Files in `public/` are served from the application root, so `public/favicon.svg` is available at `/favicon.svg`.
**src/app/layout.tsx**
```tsx
import type { Metadata } from "@farm.js/core";
export const metadata: Metadata = {
icons: "/favicon.svg",
};
```
Use the object form when you need multiple browser icons or an Apple touch icon:
```tsx
import type { Metadata } from "@farm.js/core";
export const metadata: Metadata = {
icons: {
icon: [{ url: "/favicon.svg", type: "image/svg+xml", sizes: "any" }],
shortcut: "/favicon.ico",
apple: [{ url: "/apple-touch-icon.png", type: "image/png", sizes: "180x180" }],
},
};
```
Root layout metadata applies the favicon to every route. Nested layouts and pages can override individual icon entries through their own metadata. Do not render a `` element from the layout component; declaring `metadata.icons` lets Farm place the tags in the document head in both development and production.
### Site verification
Search consoles confirm site ownership with a meta tag. Set the tokens once in the root layout:
**src/app/layout.tsx**
```tsx
import type { Metadata } from "@farm.js/core";
export const metadata: Metadata = {
verification: {
google: "google-token",
bing: "bing-token",
yandex: "yandex-token",
other: { "facebook-domain-verification": "facebook-token" },
},
};
```
`google`, `bing`, and `yandex` render `google-site-verification`, `msvalidate.01`, and
`yandex-verification`. `other` renders one `` per key. Every field accepts an
array when a service needs more than one token. Like `openGraph`, `verification` merges one level
deep, so a nested layout or page adds tokens and replaces the ones it repeats.
### Application metadata routes
Use server-only metadata files when crawlers or browsers need an application-level document rather than an HTML `` tag. Farm discovers five conventions in `src/app` and route segments:
| File | Public route | Default return type |
| -------------- | ----------------------- | ------------------------ |
| `sitemap.ts` | `/sitemap.xml` | `MetadataRoute.Sitemap` |
| `robots.ts` | `/robots.txt` | `MetadataRoute.Robots` |
| `manifest.ts` | `/manifest.webmanifest` | `MetadataRoute.Manifest` |
| `llms.ts` | `/llms.txt` | `MetadataRoute.LlmsTxt` |
| `llms-full.ts` | `/llms-full.txt` | `MetadataRoute.LlmsTxt` |
The default export can be a literal value or a sync or async function. Functions receive the matched `params`, the current `Request`, its `URLSearchParams`, and the concrete route-segment `path`.
**src/app/sitemap.ts**
```ts
import type { MetadataRoute } from "@farm.js/core";
export const revalidate = 3600;
export default async function sitemap(): Promise {
const products = await listProducts();
return [
{
url: "https://acme.test",
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
},
...products.map((product) => ({
url: `https://acme.test/products/${product.id}`,
lastModified: product.updatedAt,
priority: 0.8,
})),
];
}
```
Farm escapes XML values and supports language alternates through `alternates.languages`.
**src/app/robots.ts**
```ts
import type { MetadataRoute } from "@farm.js/core";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/admin/", "/preview/"],
},
sitemap: "https://acme.test/sitemap.xml",
host: "https://acme.test",
};
}
```
`robots.txt` controls crawling. Per-page indexing directives belong in `metadata.robots`, which
renders a `` tag. Besides `index` and `follow`, it accepts `noarchive`,
`nosnippet`, `noimageindex`, `max-snippet`, `max-image-preview`, `max-video-preview`, and
`unavailable_after`. `googleBot` takes the same directives, or a string, for a separate
`` tag:
```tsx
import type { Metadata } from "@farm.js/core";
export const metadata: Metadata = {
robots: {
index: true,
follow: true,
"max-snippet": -1,
"max-image-preview": "large",
googleBot: { noimageindex: true },
},
};
```
A root `robots.ts` replaces the robots.txt that [`agent.crawlers`](/docs/configuration#ai-crawlers)
generates, and a `public/robots.txt` wins over both. All three take `/robots.txt` from the docs
engine.
**src/app/manifest.ts**
```ts
import type { MetadataRoute } from "@farm.js/core";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Acme Store",
short_name: "Acme",
description: "The Acme product catalog",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#16a34a",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}
```
**src/app/llms.ts**
```ts
import type { MetadataRoute } from "@farm.js/core";
export default function llms({
pages,
defaults,
}: MetadataRoute.LlmsTxtContext): MetadataRoute.LlmsTxt {
return {
...defaults,
sections: [
{ title: "Docs", links: pages.filter((page) => page.path.startsWith("/docs")) },
{ title: "Optional", links: [{ title: "Changelog", url: "https://acme.test/changelog" }] },
],
};
}
```
`llms.ts` renders the [llmstxt.org](https://llmstxt.org) format: `title` as the H1, `summary` as a
quote, `details` as Markdown, and each section as an H2 list of links. To write the whole file
yourself, return it as a string instead; Farm serves it as the complete file and adds a final newline
when it is missing. Besides the usual arguments, its function receives `pages`, the app's static
pages, each linking to its Markdown mirror when `md` exposes one and to the page itself otherwise, and
`defaults`, the index [`agent.llmsTxt`](/docs/configuration#agent-readiness) would serve, so it can
extend the generated file instead of starting over. An `llms.ts` replaces `agent.llmsTxt` at its path.
`llms-full.ts` serves `/llms-full.txt` the same way. When it returns the structured format, Farm
inlines the Markdown mirror of every same-origin link that has one; a string is served as the
complete file, with a final newline added when missing. Its function
also receives `markdown(url)`, which reads a page's mirror without the request's cookies and returns
`null` when the page is unavailable.
```ts
// src/app/llms-full.ts: reuse the llms.txt structure and let Farm inline each page
export { default } from "./llms";
```
Farm automatically adds the nearest discovered manifest to rendered page heads unless `metadata.manifest` already supplies an explicit URL. A nested file keeps its route prefix: `src/app/docs/sitemap.ts` is served at `/docs/sitemap.xml`, and a file under `[tenant]` receives the concrete tenant param.
With the [docs engine](/docs/docs-engine) enabled, a root `sitemap.ts`, `robots.ts`, `llms.ts`, or `llms-full.ts` takes its path from the docs engine, which keeps serving the ones the app does not define. An app sitemap replaces the docs engine's instead of extending it, so list the docs pages in it when crawlers should find them there.
Generated metadata routes accept `GET` and `HEAD` and return `405` for other methods. They revalidate by default. Export `revalidate = 300` for shared CDN caching or `revalidate = false` only for permanently immutable output. A returned `Response` is an escape hatch for custom XML, headers, or status codes.
`feed.ts` is not reserved yet because feeds need an explicit RSS, Atom, or JSON Feed contract. Use an API or programmatic route for feeds until that format is defined.
### Alternate representations
Advertise other formats of a page, such as that feed route, with `alternates.types`. Each MIME
type renders ``, with an optional `title`:
```tsx
import type { Metadata } from "@farm.js/core";
export const metadata: Metadata = {
alternates: {
types: {
"application/rss+xml": [{ url: "/blog/feed.xml", title: "Acme blog" }],
"application/atom+xml": "/blog/atom.xml",
},
},
};
```
Relative URLs resolve against `metadataBase`, the same as `alternates.languages`. A page that sets
`alternates.types` replaces its layout's whole `types` object.
When a page has a [Markdown mirror](/docs/markdown), Farm adds
`` to its head next to the `Link`
response header it already sends. Set `alternates.types["text/markdown"]` to advertise a different
URL. Restricting `md.expose` or setting `md: false` removes the link along with the mirror.
### Static metadata images
Place `opengraph-image.png` next to a page or layout segment for a zero-code, route-local social image. Farm supports `.png`, `.jpg`, `.jpeg`, `.gif`, and `.webp` files.
```txt
src/app/
opengraph-image.png
products/
opengraph-image.png
[id]/
page.tsx
```
The root image is the application fallback. `/products/opengraph-image.png` applies to product pages and their descendants, while unrelated routes continue using the root image. Farm automatically reads the image dimensions and content type.
Add accessible preview text with an optional sidecar file:
**src/app/products/opengraph-image.alt.txt**
```txt
Acme product catalog preview
```
Farm serves the file through the extensionless `/products/opengraph-image` endpoint and adds a content fingerprint to the URL emitted in page metadata. Fingerprinted requests receive immutable caching, while direct unversioned requests revalidate with an `ETag`.
Use `twitter-image.png` when X/Twitter needs a different image. Otherwise, social platforms can use the Open Graph image. An explicit `metadata.openGraph.images` or `metadata.twitter.images` value always wins over a matching file.
Static images inside a dynamic segment are shared by every concrete route. Use a generated image when the preview must depend on route params.
### Generated metadata images
Generated JSX metadata images currently use the React image renderer. Every renderer can use static
`opengraph-image.png`, `twitter-image.png`, and explicit metadata image URLs.
Place `opengraph-image.tsx` or `twitter-image.tsx` next to a route segment when the image needs data or route params. Return ordinary stateless JSX: Farm owns the image endpoint and PNG renderer, so the component does not import `ImageResponse` or declare an API URL. Farm also adds the nearest matching image to the page head when `openGraph.images` or `twitter.images` is not already set.
**src/app/products/[id]/opengraph-image.tsx**
```tsx
import type { PageProps } from "@farm.js/core";
export const size = { width: 1200, height: 630 };
export const alt = "Product preview";
export const revalidate = 300;
function ProductCard({ name, id }: { name: string; id: string }) {
return (
Acme
Product {id}{name}
);
}
export default async function ProductOpenGraphImage({ params }: PageProps) {
const product = await getProduct(params.id);
return ;
}
```
For `/products/42`, Farm calls the component with `params.id === "42"`, serves the generated PNG at `/products/42/opengraph-image`, and emits `og:image`, `og:image:width`, `og:image:height`, and `og:image:alt` tags. A nested page such as `/products/42/reviews` inherits this image until a nearer route segment defines its own. The component and its data-loading code remain on the server.
`className` supports the image renderer's Tailwind utility set, including arbitrary values. Inline `style` can be used with it and wins when both set the same property. This is not a browser screenshot: image JSX supports flex layout, typography, borders, gradients, absolute positioning, and embedded images, but not CSS Grid, animations, media queries, pseudo-elements, hooks, or stateful and class components. The application stylesheet and custom Tailwind plugins are not executed. Use an absolute URL for an `` source.
Farm uses 1200 by 630 when `size` is omitted and includes Geist as the default font. Export `fonts` to embed a custom brand font:
```tsx
const brandFont = fetch("https://cdn.example.com/fonts/Brand-Bold.ttf").then((response) =>
response.arrayBuffer(),
);
export const fonts = [
{ name: "Brand", data: brandFont, weight: 700 as const, style: "normal" as const },
];
```
Satori-compatible TTF, OTF, and WOFF files are supported; WOFF2 is not. Set `fontFamily: "Brand"` on the element that uses the font.
Generated images revalidate on every request by default. Export `revalidate = 300` to let a CDN cache the route for five minutes, or `revalidate = false` only when the output is permanently immutable. Farm emits an `ETag`, supports conditional requests and `HEAD`, and performs JSX-to-image rendering internally.
For advanced renderers, the default export may still return a `Response`, string, or bytes. To preserve the earlier React-to-SVG behavior, export `contentType = "image/svg+xml"` and return an SVG React element. A returned `Response` keeps its own status, headers, and body.
Keep only one implementation for each image kind in a segment. For example, defining both `opengraph-image.png` and `opengraph-image.tsx` produces a build error. For broad social-platform compatibility, use 1200 by 630; generated JSX routes emit PNG automatically.
### Image responses in API routes
When an image needs its own URL, such as a share card per record or a badge another site embeds, return `ImageResponse` from an API route. `@farm.js/core/og` exports the same `@vercel/og` renderer that generated metadata images use, so the app does not install it separately.
**src/app/api/og/route.tsx**
```tsx
import { ImageResponse } from "@farm.js/core/og";
export async function GET(request: Request) {
const title = new URL(request.url).searchParams.get("title") ?? "Acme";
return new ImageResponse(
{title}
,
{ width: 1200, height: 630 },
);
}
```
`ImageResponse` is a standard `Response` with a PNG body. Its options take `width`, `height`, `fonts`, `emoji`, and `debug` alongside the usual `status` and `headers`. The element goes to the renderer as written, so style it with `style` or the `tw` prop; the `className` conversion described above applies only to metadata image files. Like generated metadata images, it takes React JSX.
Outside development the response defaults to `cache-control: public, immutable, no-transform, max-age=31536000`. Pass a lowercase `cache-control` header when the image can change, for example `headers: { "cache-control": "public, max-age=300" }`.
`@farm.js/core/og` is server-only. Importing it from a client component fails the build, and the development request, with an error that names the importing module. On Node presets, including `node-server` and Vercel Functions, Farm copies `@vercel/og` and its wasm and font files into the server output whenever the server bundle imports it. Edge and Cloudflare Worker presets are not supported yet.
### Other meta tags
Use `other` for a `` tag that has no dedicated metadata field. Each key renders one tag,
and an array value renders one tag per item:
```tsx
import type { Metadata } from "@farm.js/core";
export const metadata: Metadata = {
other: {
"apple-itunes-app": "app-id=123456789",
"format-detection": "telephone=no",
},
};
```
Names and values are escaped. `other` merges one level deep, so a page can replace a key its layout
set and keep the rest. Client navigation removes the previous page's `other` tags before adding the
next page's.
## File Route States
Use `loading.*` and `error.*` next to a file route to define route-local loading and error states.
Use `.tsx`/`.jsx` with React, Preact, or Solid, `.vue` with Vue, and `.svelte` with Svelte. Farm picks the
nearest matching boundary, so `src/app/dashboard/error.tsx` handles `/dashboard` and nested dashboard
pages unless a deeper segment defines its own boundary.
```txt
src/app/
dashboard/
page.tsx
loading.tsx
error.tsx
```
**src/app/dashboard/loading.tsx**
```tsx
import type { LoadingProps } from "@farm.js/core";
export default function DashboardLoading(props: LoadingProps) {
return
Loading {props.path}
;
}
```
`loading.tsx` is used as the Suspense fallback when the page or nested content suspends while rendering.
**src/app/dashboard/error.tsx**
```tsx
"use client";
import type { ErrorProps } from "@farm.js/core";
export default function DashboardError({ error, reset }: ErrorProps) {
const message = error instanceof Error ? error.message : "Something went wrong";
return (
Could not load dashboard
{message}
);
}
```
`error.tsx` receives `error`, `reset`, `params`, `path`, `search`, `searchParams`, middleware data, and plugin context. The closest route error boundary handles normal render/data failures. Redirects and `notFound()` still escape to Farm's redirect and not-found handling.
### redirect() and notFound()
Import `redirect()`, `permanentRedirect()`, and `notFound()` from `@farm.js/core` or
`@farm.js/core/navigation`. They work in pages, layouts, server components, route data hooks, and
[middleware](/docs/middleware), and a page that hydrates can import them like any other module.
```tsx
import { notFound, type PageProps } from "@farm.js/core";
export default async function ProductPage({ params }: PageProps<"/products/[id]">) {
const product = await getProduct(params.id);
if (!product) notFound();
return ;
}
```
While the response has not started, Farm answers with the real status: `307` or `308` with a
`Location` header, or `404` with the app's configured not-found page. That includes a synchronous page
under a `loading.tsx` boundary, and an async page in production, which Farm awaits before rendering.
When the call happens after a loading boundary already streamed the `200` shell (a component behind
`` that finishes later, or an async page in development), the status can no longer
change. Like Next.js, Farm finishes the document so it recovers in the browser: an `http(s)`
redirect replaces the location, and `notFound()` swaps the page for the app's not-found page,
marks the document `noindex`, and skips hydrating the page that threw. The layouts are already on
screen, so the not-found page renders without them; if it needs context a layout provides, Farm
shows its built-in not-found page instead.
With experimental RSC enabled, failures before the HTML shell is sent render the nearest
`error.tsx` through RSC and SSR with status `500` and `Cache-Control: private, no-store`.
RSC loading and error boundaries follow the selected page's file ancestors, including route
groups and catch-all folders. For example, `/users/[id]/error.tsx` does not handle a sibling
`/users/new/page.tsx`. Failures before page selection, such as middleware errors, use only the
root error boundary when one exists.
This fallback uses a standalone document shell, outside the failed page/layout tree, so a broken
layout cannot prevent the error UI from rendering. Client boundaries receive a client-owned
`reset()` that reloads the current URL. Production responses show a generic error message;
the original exception stays in server logs. Development responses can show the error message.
If no boundary exists, or the boundary itself fails during SSR, Farm returns a generic non-cacheable
500 response. Failures after streaming starts cannot change the already-sent status code and remain
subject to React's streaming recovery behavior.
Place `not-found.*` at the app root to customize unmatched URLs. When the component lives elsewhere,
set its project-relative path explicitly; Farm uses the same file in development and production and
fails startup or build when the path is missing or incompatible with the selected renderer:
```ts title="farm.config.ts"
export default defineConfig({
notFound: {
component: "./src/ui/not-found.tsx",
},
});
```
## Catch-all routes
Catch-all routes are useful for docs, CMS content, and nested marketing pages where the page is resolved from content instead of a fixed file for every URL.
**src/app/docs/[...slug]/page.tsx**
```tsx
import type { PageProps } from "@farm.js/core";
export default function DocsPage({ params }: PageProps<"/docs/[...slug]">) {
const slug = params.slug.split("/");
return Docs path: {slug.join(" / ")};
}
```
## Route groups
Use route groups to organize files without adding URL segments. They are useful when an app has multiple shells.
**Route groups**
```txt
src/app/
(marketing)/
page.tsx
pricing/
page.tsx
(app)/
dashboard/
page.tsx
```
The group names are organizational. The URLs are still `/`, `/pricing`, and `/dashboard`.
## Pending Navigation UI
Use `useNavigation()` for global route-transition UI: top progress bars, disabled navigation buttons, optimistic shells, or app-wide busy indicators. It tracks SPA navigations started by `Link` and `navigateTo`.
```tsx
"use client";
import { useNavigation } from "@farm.js/core/client";
export function TopProgress() {
const navigation = useNavigation();
return ;
}
```
`navigation.state` is `"loading"` while route data is being fetched and returns to `"idle"` after the route is committed. `navigation.to` includes the target `pathname`, `search`, `hash`, and full `href`. Use route `loading.tsx` for segment-level Suspense fallbacks, and `useNavigation()` when the surrounding app shell should react to a navigation.
## View Transitions
Pass `viewTransition` to `Link` or `navigateTo` when a navigation should use the browser View Transitions API. Farm starts the transition after route data is ready and falls back to normal SPA navigation when the browser does not support it.
```tsx
import { Link, navigateTo } from "@farm.js/core/client";
export function GalleryLink() {
return (
Gallery
);
}
await navigateTo("/gallery", { viewTransition: true });
```
Use this for image galleries, dashboards that keep the same app shell, settings panes, and modal-to-page flows. Keep important loading states in `loading.tsx` or `useNavigation()`; view transitions are visual polish, not a loading boundary.
## Navigation workflow
1. Add or rename route files.
2. Keep `farm dev` running; Farm regenerates route and API types when route files change.
3. Use `Link` for internal navigation and plain anchors for external URLs.
4. Keep dynamic route values encoded in the href string, such as `/blog/${slug}`.
Run `farm generate` when you want to refresh generated types outside the dev server, such as in CI or after a large file move.
---
## Server Queries
URL: /docs/server-queries
Define typed server reads once, deduplicate requests, prefetch browser data, use stale-while-revalidate, and invalidate route, API, and query consumers with one key.
# Server Queries
`createServerQuery` defines a typed server read with one structured cache key. The same declaration works during server rendering, through a generated browser server reference, with browser prefetch, and in `useServerQuery`.
The browser lifecycle is intentionally familiar to React Query and TanStack Query users: request
deduplication, prefetching, `staleTime`, stale-while-revalidate, focus and reconnect refresh, shared
invalidation, and optimistic cache writes. Farm implements this behavior in its own cache and
generated server references; it does not install or wrap TanStack Query.
> **Browser usage requires the server-function transform**
>
> The generated browser server references come from the `@farm.js/plugin/rsc` transform, enabled by
> adding the plugin and setting `experimental.serverActions: true` in `farm.config.ts`. Without it,
> `useServerQuery`, `prefetchServerQuery`, and `fetchServerQuery` can only run during server
> rendering: importing a query module from `"use client"` code would bundle the server handler into
> the browser, and Farm fails the build with a boundary error instead. In apps without the
> transform, call queries from server components, or expose an API route and use
> [`apiClient` from `createApiClients()`](/docs/api-client) from client components.
## Declare a query
**src/features/products/queries.ts**
```ts
import { createServerQuery } from "@farm.js/core/server-query";
import { z } from "zod";
const Product = z.object({
id: z.string(),
name: z.string(),
price: z.number(),
});
export const productQuery = createServerQuery({
input: z.object({ id: z.string() }),
output: Product,
key: ({ input }) => ["product", input.id],
staleTime: "30s",
async handler({ input, request, signal, context }) {
return db.product.findUniqueOrThrow({
where: { id: input.id },
});
},
});
```
Farm adds the server-module boundary during compilation. Keep exported `createServerQuery` declarations out of files with `"use client"`, just like `createServerFn` declarations.
The `input`, `output`, and middleware contracts are the same as `createServerFn`. Input is validated before the key and handler run. Output is validated before it enters either the server or browser cache.
## Call on the server
```tsx
import { productQuery } from "@/features/products/queries";
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await productQuery({ id: params.id });
return
{product.name}
;
}
```
Matching calls in one render request share the same in-flight promise. The handler runs once and every caller receives the validated result.
## Prefetch in the browser
```tsx
"use client";
import { prefetchServerQuery } from "@farm.js/core/server-query/client";
import { productQuery } from "@/features/products/queries";
export function ProductLink({ id }: { id: string }) {
return (
prefetchServerQuery(productQuery, { id })}>
Open product
);
}
```
Concurrent prefetches and mounted consumers use one browser request. A successful prefetch is immediately available to `useServerQuery`.
## Read with browser SWR
```tsx
"use client";
import { useServerQuery } from "@farm.js/core/server-query/client";
import { productQuery } from "@/features/products/queries";
export function ProductPrice({ id }: { id: string }) {
const product = useServerQuery(productQuery, { id });
if (product.pending) return ;
if (product.error && !product.data) return
{product.error.message}
;
return (
{product.data?.price}
);
}
```
The hook returns `data`, `error`, `status`, `pending`, `fetching`, `stale`, and `refetch`. Stale data remains visible while Farm refreshes it in the background. Stale queries also refresh on window focus and reconnect unless those options are disabled.
Set `enabled: false` to pause automatic reads, invalidation refetches, and focus/reconnect refresh.
An already-running read is not cancelled. Switching back to `enabled: true` checks the cache again:
missing or stale data refreshes, fresh data is reused, and pending work is deduplicated. This does
not add polling or continuously refetch when `staleTime` is zero. Explicit `refetch()` remains
available while automatic reads are disabled.
`refetch()` always starts fresh work. If an older request finishes afterward, its result is returned
to its original caller but cannot replace the newer cached value.
Invalidating a query while its read is pending keeps that response stale, even if the response
timestamp is newer than the invalidation or its canonical key is learned only on arrival.
The original caller still receives its result. Enabled mounted consumers refresh after the old
work settles; an imperative read with `swr: false` waits for fresh data on the next call.
Unrelated invalidations and invalidations before a read starts do not invalidate that read.
Broadcast invalidations for keys a client cache does not own are retained only by active
request-lifetime trackers, not indefinitely by every cache. Cached, subscribed, and pending keys
still receive invalidations, including keys reached through provisional aliases.
Use `fetchServerQuery(productQuery, input)` for an imperative browser read that should participate in deduplication and SWR. Calling the generated `productQuery(input)` reference directly still returns plain typed data, but the fetch helper supplies the browser cache lifecycle.
With SWR enabled (the default), every imperative reader receives the existing stale value
immediately while one shared refresh runs, including readers arriving after that refresh starts.
An initial read without cached data still waits. Set `swr: false` to wait for the in-flight
result, or `force: true` to start and await a new request instead of joining it.
## Invalidate after a mutation
```ts
import { createServerFn } from "@farm.js/core/server-fn";
import { z } from "zod";
export const updateProduct = createServerFn({
input: z.object({
id: z.string(),
name: z.string().min(1),
}),
invalidates: ({ input }) => [{ key: ["product", input.id] }],
async handler({ input }) {
await db.product.update({
where: { id: input.id },
data: { name: input.name },
});
return { ok: true };
},
});
```
During a browser server-action call, Farm carries structured invalidations back with the action response. Mounted stale queries refetch automatically, and concurrent consumers still produce one request.
If invalidation arrives while a query is still resolving its canonical server key, the invalidation
follows that key and the older response cannot make the entry fresh again.
If an automatic refresh fails, `useServerQuery` keeps the previous data stale and exposes the
error without repeatedly retrying the same invalidation. Call `refetch()` to retry, or let a new
invalidation, focus/reconnect event, or re-enabled query trigger another read. Concurrent mounted
consumers continue to share that read.
## Share keys with routes and APIs
Server queries use the existing route-data cache namespace and tag. They do not create a separate server cache.
For a key used in several features, keep its factory in a module that is safe to import from both
the browser and server. Plain string and array keys remain the default and require no helper:
```ts
export const productKey = (id: string) => ["product", id] as const;
```
Use `defineCacheKey` only when you want TypeScript to carry the value stored under that key into
optimistic cache updaters:
```ts
import { defineCacheKey } from "@farm.js/core/cache";
import type { Product } from "./types";
export const productKey = defineCacheKey()((id: string) => ["product", id] as const);
```
The helper adds no wrapper object or runtime cache format. `productKey("123")` is still the raw
`["product", "123"]` array, so typed and untyped keys interoperate and existing applications do
not need to migrate.
Use the same structured key in programmatic route data:
```ts
data: {
key: ({ params }) => ["product", params.id],
staleTime: "30s",
async main({ params }) {
return getProduct(params.id);
},
}
```
Use it in an API client cache when the API response has the same data contract:
```ts
const result = await api.products.get(
{ query: { id } },
{
cache: {
key: ["product", id],
policy: "stale-while-revalidate",
staleTime: 30_000,
},
},
);
```
The route, API caller, and server query now read and invalidate the same canonical key. Default API keys remain isolated by request origin; only an explicit structured key opts into cross-feature sharing.
When overlapping server-query requests use different function references but resolve to the same
canonical key, a late older response cannot replace a newer request's cached result. Each caller
still receives its own result. Once both references' keys are known, newer pending and error states
are protected too. A key cannot be associated with a previously unknown reference until a Farm
transport returns its cache metadata; plain-data transports remain scoped to the function and input.
Expired shared-cache entries keep their aliases and invalidation marks while a related subscription
or request is active. Releasing an owner rechecks that key without scanning unrelated pending
entries. Retargeting an alias also releases the previous target's metadata once it has no owner.
### Share optimistic updates
API mutations can optimistically update data watched by `useServerQuery` when both features use the
same structured key and the same data shape:
```ts
const products = await api.products.get(
{ query: { category } },
{
cache: {
key: ["products", category],
policy: "stale-while-revalidate",
staleTime: 30_000,
},
},
);
await api.products.post(
{ body: draft },
{
optimistic: {
update: [
[
products.key,
(current) => ({
...current,
products: [{ ...draft, id: "optimistic" }, ...(current?.products ?? [])],
}),
],
],
rollbackOnError: true,
},
invalidate: [products.key],
},
);
```
This works through the shared Farm client cache; `useServerQuery` does not expose a separate
optimistic option. Keep the API response and server-query result contracts identical whenever they
share a key.
Server-function mutations reach the same shared cache through `useMutation`: pass
key-targeted `request.optimistic` updates and `request.invalidate` targets, as described in
[Track mutations in React](/docs/api-client#track-mutations-in-react).
## Cache lifetime
| `staleTime` | Server behavior | Browser behavior |
| ----------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| omitted or `0` | Deduplicate within the current request without retaining the result | Treat a result as immediately stale and refresh it when read again |
| duration such as `"30s"` or `30000` | Store in the shared route-data cache until stale | Return fresh data until the duration expires, then use SWR |
| `false` | Store until explicit invalidation | Keep fresh until explicit invalidation |
Numbers are milliseconds. Duration strings support `ms`, `s`, `m`, and `h`. Failed handlers and invalid output are never cached.
When the app configures a [client cache adapter](/docs/api-client#persist-the-client-cache),
declare `persist: true` on a query to allow its results to be stored on the device and rendered
stale-but-visible on the next cold start. Nothing persists without the flag; treat it as part of
the query's security review, since persisted results outlive the session.
## Middleware and cancellation
Queries accept the same composable middleware as server functions:
```ts
export const accountQuery = createServerQuery({
middleware: [requireSession],
input: z.object({ accountId: z.string() }),
key: ({ input, context }) => ["account", context.user.id, input.accountId],
async handler({ input, context, signal }) {
return getAccount(input.accountId, context.user.id, { signal });
},
});
```
Authentication middleware still runs for every query invocation, including cache hits. The request signal aborts when the underlying server-action request is cancelled.
## Security practices
- Treat a server-query reference as transport, not authorization. Verify authentication, role, tenant, and resource ownership on the server.
- Include every identity that changes the result in a persistent key. For private data, use keys such as `["account", context.user.id, accountId]`, not only `["account", accountId]`.
- Prefer request-only caching by omitting `staleTime` when a safe shared persistent key is not available.
- Keep output schemas narrow so private database fields cannot enter the browser cache accidentally.
- Never place secrets, tokens, or raw session objects in keys; keys can appear in diagnostics and invalidation metadata.
- Use API routes instead of server queries for intentionally cross-origin or public HTTP contracts.
## Production practices
- Keep keys small, deterministic, and serializable.
- Use one key for one data shape. Route data, API responses, and queries should share a key only when their cached value contracts match.
- Prefetch on strong intent such as pointer focus, viewport proximity, or an immediately likely next step.
- Render stale data with a quiet `fetching` state instead of replacing useful content with a full loading screen.
- Invalidate immediately after the database transaction succeeds.
- Return typed expected states such as `notFound` or `forbidden`; reserve thrown errors for unexpected failures.
---
## Rendering Model
URL: /docs/server-rendering
Choose dynamic rendering, static rendering, ISR, or PPR with route-level exports and config.
# Rendering Model
Choose dynamic rendering, static rendering, ISR, or PPR with route-level exports and config.
Rendering controls decide when HTML is produced. [Route Runtime](/docs/route-runtime) separately decides where a dynamic page or API handler executes and how deployment limits are applied.
Dynamic rendering, static rendering, and revalidation apply to every renderer. The component
examples below use React-compatible TSX; see [Renderers](/docs/renderers) for Preact, Solid, Vue, and
Svelte conventions and the features that remain React-specific.
## Rendering options
RSC page URLs return HTML for document visits and a Flight payload for requests accepting
`text/x-component`. Both representations include `Vary: Accept`. When you enable shared caching,
configure the CDN or reverse proxy to honor `Vary`; do not cache these responses by URL alone.
Farm preserves existing `Vary` fields, including `Origin` and Nitro's `Accept-Encoding`.
| Mode | How to opt in | Best for |
| ------- | ---------------------------------------------- | --------------------------------------- |
| Dynamic | Default for request-bound pages | Dashboards and personalized UI. |
| Static | dynamic = force-static or use static directive | Marketing pages and stable docs. |
| ISR | revalidate = seconds | Content that can refresh on a schedule. |
| PPR | experimental_ppr = true | Static shells with dynamic holes. |
## Route-level config
**src/app/pricing/page.tsx**
```tsx
export const dynamic = "force-static";
export const revalidate = 300;
export default async function PricingPage() {
return Pricing;
}
```
## Use directives when compactness wins
Farm also recognizes compact rendering directives at the top of route modules. This keeps small examples readable while preserving explicit exports for Next-style compatibility.
**src/app/blog/page.tsx**
```tsx
"use ssg; 60";
export default function BlogPage() {
return Blog;
}
```
`use ssg; 60` statically generates the route and revalidates it every 60 seconds. Farm also accepts
`use ssg`, `use dynamic`, and `use ppr; 60` when those rendering modes fit the route.
## Dynamic rendering
Use dynamic rendering for request-specific pages such as dashboards, account settings, and pages that depend on cookies, headers, or per-user data.
With experimental React Server Components enabled, Farm streams HTML while inserting the styles,
hydration payload, and client script. HTML injection preserves UTF-8 bytes across stream chunks,
including emoji and non-ASCII content, without buffering the entire page.
**src/app/dashboard/page.tsx**
```tsx
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
return Dashboard;
}
```
## Static rendering
Use static rendering for pages that can be built once and served quickly.
During a production build, Farm also scans routes that use the default dynamic mode. If a
non-parameterized route does not read cookies, headers, authentication, search parameters, or
middleware request data, Farm reports it as a static-rendering candidate. The build keeps the route
dynamic until you opt in, because request-bound work may be hidden in an imported component or data
loader.
Review each suggestion, including its imported code, then add the static export when it is safe. This
keeps personalized pages dynamic while making stable pages easy to move onto the fastest rendering
and caching path.
If an explicitly static route directly reads request APIs or request props, Farm reports the unsafe
read and keeps the route dynamic instead of freezing request-specific HTML into a shared artifact.
This check covers the route module itself; continue reviewing imported components and data loaders,
because request-bound work in transitive imports cannot be proven safe from the route source alone.
Farm serves generated HTML only for `GET` and `HEAD` requests. Other request methods continue through
the live route instead of receiving a cached page document.
**src/app/about/page.tsx**
```tsx
export const dynamic = "force-static";
export default function AboutPage() {
return About Farm;
}
```
## ISR-style revalidation
`revalidate` caches a static response and refreshes it after the configured number of seconds.
**src/app/pricing/page.tsx**
```tsx
export const dynamic = "force-static";
export const revalidate = 300;
export default async function PricingPage() {
const plans = await loadPlans();
return ;
}
```
## PPR with Suspense
PPR is for pages with a stable shell and dynamic sections. Put dynamic work behind Suspense boundaries so the shell can be cached while the slower section resolves independently.
**src/app/dashboard/page.tsx**
```tsx
import { Suspense } from "react";
export const experimental_ppr = true;
export const revalidate = 60;
export default function DashboardPage() {
return (
Dashboard
}>
);
}
```
## Deferred route islands
The route-level hydration strategy is shared by renderer adapters. Preact uses the same nested
`"use client"` analysis through its compatibility runtime; Solid, Vue, and Svelte currently hydrate
at the route boundary selected by their adapter.
Client routes and server routes that import a client boundary can defer their JavaScript while Farm
keeps the server-rendered HTML visible. Add an optional static `island` export to the client module:
```tsx
"use client";
export const island = "interaction";
export function CopyButton({ value }: { value: string }) {
return ;
}
```
Farm propagates the strategy to the route hydration boundary, emits it in the route manifest, and
splits production route modules behind dynamic imports. Deferred routes therefore avoid evaluating
their route chunk until its trigger while preserving the initial SSR output.
These strategies control hydration of the initial server-rendered document. During client-side
navigation, the navigation itself signals user intent, so Farm loads and renders a route-wide
destination immediately instead of leaving the previous route visible while waiting for another
trigger.
| Strategy | Hydration trigger |
| ------------- | --------------------------------------------------------------------- |
| `load` | Immediately. This is the default and compatibility-first behavior. |
| `interaction` | The first button-like click; Farm replays that click after hydration. |
| `visible` | When the route boundary approaches the viewport. |
| `idle` | During browser idle time, with a timeout fallback. |
With [isolated client hydration](/docs/configuration#isolated-client-hydration), each eligible
client module keeps its own strategy, including after SPA navigation. Sibling boundaries can mix
all four strategies: one boundary's trigger never hydrates another, and an interaction click is
claimed and replayed once by the boundary that contains it. Removing a boundary cancels its pending
observer, idle callback, or interaction listener.
Farm uses the same isolated boundary metadata for Vite development, streamed or buffered SSR, and
statically generated HTML. In development, updating a client module rerenders every live boundary
created from that module without importing or replacing its server-owned layout; sibling boundary
state stays mounted. If one page needs the route-wide fallback, that fallback stays on the page
boundary and does not promote otherwise eligible client leaves in its server-owned layout.
Farm also keeps graphs above four statically bounded isolated roots route-wide. Data-dependent
boundary lists use the same fallback because their root count is unknown before streaming. This
measured guard prevents request, marker, root, and hydration overhead from growing past the first
observed crossover; the [benchmark report](https://github.com/farming-labs/farm.js/blob/main/benchmarks/isolated-hydration/results/latest.md) includes route-wide, isolated, and RSC controls with raw samples.
The representative fixtures also run route-wide and isolated hydration with Farm's experimental
React compiler enabled, proving both the initial hydration cost and repeated state-update cost of
the combined path.
The export must be one of these static string literals so Farm can analyze it without executing
application code. Without an explicit route-level `island` export, a route that imports client
boundaries with different strategies safely falls back to `load` because its current route-level
React-compatible root cannot schedule those children independently. Keep interactive leaves small today; a
eligible leaves can also use Farm's experimental React compiler for direct state binding updates
after their independent roots mount.
### Async pages and client components
A page or layout can be an `async` server component and still render `"use client"` components that
work in the browser:
```tsx
import { StarButton } from "./star-button";
export default async function RepoPage() {
const repo = await getRepo();
return (
{repo.name}
);
}
```
React cannot run an `async` component in the browser, so the page itself stays server-rendered and
never ships to the client. Each eligible client boundary it renders hydrates as its own island, with
the props the server passed it. This works by default in React apps; it does not need
`experimental.isolatedClientHydration`. Under a parent layout that hydrates as a whole route, the
layout keeps the async page as server HTML and the islands inside it still hydrate, including
after client navigation between pages of that layout.
Navigating away cancels pending island work and unmounts its roots without waiting for hydration.
React's early-update diagnostic during that synchronous cancellation is expected; hydration
mismatches, updates to live roots, and other errors are still reported.
Islands have the same limits as [isolated client leaves](#isolated-client-leaves-without-rsc):
- Props must be serializable. Passing server-rendered JSX as `children` or another prop
(``) does not cross the boundary.
- A client component must use a supported export shape and must not sit where the HTML parser moves
it, such as directly inside `
`.
- An integration provider that wraps the whole route, and does not declare
`supportsIsolatedHydration: true`, keeps its routes route-wide.
When one of these applies, the route stays server-rendered, its client components are not
interactive, and Farm logs a warning naming the module and the reason. Fix the reason, or render
the interactive part from a synchronous page. Preact, Solid, Vue, and Svelte keep async pages
server-only.
Client components that come from a package, such as the icons in `lucide-react`, cannot be isolated
yet. An async page renders them as static HTML and still hydrates its own client components as
islands, and Farm logs which packages it left static.
### Isolated client leaves without RSC
`"use client"` also remains available in a normal React SSR application. Set
`experimental.isolatedClientHydration` to `"analyze"` to audit which local client leaves Farm can
split without changing runtime behavior, or to `"enabled"` to hydrate safe leaves independently.
Unsupported routes keep the existing route-wide ownership model. See
[Isolated client hydration](/docs/configuration#isolated-client-hydration) for the modes and safety
rules. This experiment is disabled when RSC owns the route. Integration providers also keep the
route-wide root unless they explicitly declare that they can be recreated around independent
isolated roots.
## Automatic optimized boundaries
Automatic optimized boundaries are a React-only experiment and are not applied to Preact, Solid,
Vue, or Svelte routes.
Farm can experimentally render large, non-interactive Server Component regions through the native
Strata renderer. Enable the flag once in `farm.config.ts`:
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
experimental: {
serverComponents: true,
optimizedBoundary: true,
},
});
```
Application components remain ordinary JSX:
```tsx
export default function ArticlePage() {
return (
Representation-aware rendering
Farm selects this host-only region automatically.
);
}
```
Farm evaluates eligible host-element trees on the server and uses Strata only when the region is
large enough to benefit. Trees containing Client Components, event handlers, refs, unsupported
elements or attributes, unsafe URLs, or other uncertain values retain normal React rendering. The
flag does not require an application boundary component or a direct Strata dependency.
The current Strata runtime uses a native Node binding. Keep this experiment on Node deployments until
a Wasm or JavaScript fallback is available for edge and Cloudflare worker targets.
## Choosing a mode
| Need | Use |
| -------------------------------------- | --------------------------------------- |
| User-specific data on every request | `dynamic = "force-dynamic"` |
| Stable docs, marketing, or policy page | `dynamic = "force-static"` |
| Stable page with scheduled refresh | `revalidate = 60` |
| Static shell plus dynamic holes | `experimental_ppr = true` with Suspense |
---
## Stability and Support
URL: /docs/stability
What Farm 0.1 treats as stable, beta, and experimental, which renderers and deployment targets are verified, and how breaking changes are introduced.
# Stability and Support
Farm 0.1 is the first release line with a compatibility promise. This page states what the promise covers, how each package, renderer, deployment target, and feature is classified, and how changes are introduced.
## Stability levels
| Level | What you can rely on |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Stable** | Covered by the versioning policy below. Breaking changes follow a deprecation in an earlier minor release. |
| **Beta** | Supported and tested, but the API can still change in a minor release without a deprecation cycle. Beta packages keep publishing `0.1.0-beta.N` versions. |
| **Experimental** | Opt-in only, behind an `experimental` option or an explicit flag. It can change or be removed in any release. Disabled experimental features add no runtime cost. |
## Versioning policy
Farm follows semantic versioning with the usual rules for `0.x` releases:
- **Patch releases** (`0.1.1`, `0.1.2`) fix bugs and security issues. They do not remove or change stable APIs, configuration, generated route types, or production output contracts.
- **Minor releases** (`0.2.0`) may change stable APIs. A stable API is deprecated in an earlier minor release first, keeps working with a runtime or type-level warning, and the release notes include migration steps.
- **Experimental APIs** can change in any release. The release notes call out those changes.
- Security fixes may tighten behavior in a patch release when the previous behavior was unsafe. The release notes say so explicitly.
The integration packages pin `@farm.js/core` to the exact version they were released with, so keep every package in the shared release group on the same version. `farm upgrade` does this for you.
Independently versioned beta plugins must also be republished against a stable core before a
stable release is promoted. The release publisher checks registry manifests and stops if a beta
package still resolves a prerelease core, preventing two Farm runtimes from being installed.
## Packages
**Stable, released together at the same version:**
- `@farm.js/core`, `@farm.js/cli`, `@farm.js/create-app`, `@farm.js/plugin`, `@farm.js/devtools`
- `@farm.js/integration-utils` and the `@farm.js/integrations` compatibility re-exports
- The first-party integrations: `@farm.js/ai`, `@farm.js/auth`, `@farm.js/auth0`, `@farm.js/authjs`, `@farm.js/autumn`, `@farm.js/better-auth`, `@farm.js/cf-agent`, `@farm.js/clerk`, `@farm.js/contentful`, `@farm.js/email`, `@farm.js/eve`, `@farm.js/jobs`, `@farm.js/polar`, `@farm.js/preview-gateway`, `@farm.js/sanity`, `@farm.js/sentry`, `@farm.js/strapi`, `@farm.js/stripe`, `@farm.js/supabase`, `@farm.js/unkey`, `@farm.js/workos`
An integration's stable surface is its Farm API: the factory, options, mounted routes, generated client bindings, and documented request and response shapes. Provider behavior follows the provider SDK version declared in the package's peer dependencies.
**Beta, versioned independently:**
- Renderer packages: `@farm.js/react` (the React compiler and runtime package; the default React path lives in core), `@farm.js/preact`, `@farm.js/solid`, `@farm.js/vue`, `@farm.js/svelte`
- Plugins: `@farm.js/analyzer`, `@farm.js/cache-redis`, `@farm.js/content`, `@farm.js/hints`, `@farm.js/msw`, `@farm.js/otel`, `@farm.js/partytown`, `@farm.js/preview-tunnel`, `@farm.js/pwa`, `@farm.js/scripts`, `@farm.js/search`, `@farm.js/stylex`, `@farm.js/sync`
**Experimental packages:** `@farm.js/federation`, `@farm.js/mcp`, `@farm.js/wasm`, `@farm.js/webmcp`.
## Renderers
| Renderer | Level | Package | Notes |
| -------- | ------ | -------------------------- | --------------------------------------------------------------------- |
| React | Stable | built into `@farm.js/core` | Default renderer. Used by every template unless you pick another one. |
| Preact | Beta | `@farm.js/preact` | |
| Solid | Beta | `@farm.js/solid` | |
| Vue | Beta | `@farm.js/vue` | |
| Svelte | Beta | `@farm.js/svelte` | Buffered server rendering (no streaming). |
Framework-level behavior (routing, APIs, actions, queries, middleware, deployment) is renderer-neutral and covered by the shared renderer conformance suite. See [Renderers](/docs/renderers) for the per-renderer capability matrix.
## Deployment targets
| Target | Preset | Level | How it is verified |
| ---------------------------- | ------------------ | ------------ | ---------------------------------------------------------------------------------------------- |
| `node` | `node-server` | Stable | Production browser suites in CI run against a built `node-server` output. |
| `vercel` | `vercel` | Stable | Build Output tests in CI; the Farm documentation site is deployed with it. |
| `cloudflare` | `cloudflare-pages` | Stable | Built output runs in `workerd` on every supported Node.js version in CI. |
| `netlify` | `netlify` | Stable | Built output runs through Netlify's request pipeline on every supported Node.js version in CI. |
| Direct Nitro `preset` values | any | Pass-through | Farm passes the preset to Nitro. Output is not tested by Farm unless it appears above. |
`deploy.target` values get Farm's defaults and deploy commands. A raw Nitro `preset` such as `vercel-edge`, `netlify-edge`, `cloudflare-module`, `deno`, or `bun` is supported on a best-effort basis. See [Deployment](/docs/deployment).
## Experimental features
These are off by default and outside the stability promise:
| Feature | How to enable |
| ------------------------- | ------------------------------------------------------------------- |
| React Server Components | `experimental.serverComponents`, or the `@farm.js/plugin/rsc` setup |
| Server Actions under RSC | `experimental.serverActions` |
| Partial Prerendering | `experimental.ppr`, then opt routes in |
| Isolated client hydration | `experimental.isolatedClientHydration: "analyze" \| "enabled"` |
| React compiler | `react({ experimental: { compiler: true } })` from `@farm.js/react` |
| API route MCP server | `apiMcp()` from `@farm.js/mcp` |
| WebMCP browser tools | `@farm.js/webmcp` |
| Module federation | `@farm.js/federation` |
| WebAssembly components | `@farm.js/wasm` |
## Known limits in 0.1
- **Runtime-only docs features on edge.** Farm can precompile the built-in renderer and external adapters that publish an edge compiler, but adapter features that require live server callbacks or mutable sessions still need a Node target. The official adapter reports the exact unsupported features during the build.
- **Node.js 22.13 or newer** is required for development and for Node deployment targets.
## Reporting problems
Open an issue at [github.com/farming-labs/farm.js](https://github.com/farming-labs/farm.js/issues) with the Farm version (`farm doctor` prints it), the renderer, and the deployment target.
---
## KV Storage
URL: /docs/storage
Use Farm's key/value API for caches, settings, counters, idempotency records, and object-backed values.
# KV Storage
Farm's `@farm.js/core/storage` module is a **key/value API**. Every value is stored under a string key and read with operations such as `getItem`, `setItem`, `getKeys`, and `removeItem`.
Use it for caches, feature flags, application settings, idempotency records, checkpoints, and object-backed values. Do not use it as a relational ORM for users, accounts, products, or other records that need model fields, relations, joins, or typed filters. Request-rate enforcement uses a separate atomic counter contract described below.
## Choose the right data API
| What you need | Use |
| ------------------------------------------------- | ----------------------------------------------------------- |
| Cache entries, flags, settings, counters, or JSON | Farm KV storage through `getStorage(name)` |
| Rate limiting shared across production instances | An atomic adapter such as `redisRateLimitStorage(...)` |
| Files or values addressed by one key | An object-backed KV helper such as `s3Storage(...)` |
| Application models, relations, joins, and filters | An application-owned `@farming-labs/orm` or another ORM |
| Models owned by a schema-backed Farm integration | Farm's integration ORM through `ctx.args.db` |
| Better Auth users, accounts, and sessions | Better Auth's configured database adapter and instance APIs |
| Provider-specific SQL or database operations | The raw integration runtime client through `getClient()` |
The current beta config groups two different inputs under the `storage` key:
- `storage.driver`, `storage.mounts`, and a Farm storage client configure the KV system used by `getStorage()`.
- A raw database or ORM object passed to `storage.client` is reserved for schema-backed integrations and is documented under [Database and ORM Clients](/docs/integrations/orm-storage).
These paths do not convert into each other. In particular, a raw PostgreSQL pool supplied as `storage.client` does not become the value returned by `getStorage()`, and it does not make the default in-memory KV store durable.
## Create a KV client
**src/lib/storage.ts**
```ts
import { sqliteStorage } from "@farm.js/core/storage";
export const appStorage = sqliteStorage({
path: "./.farm/storage/app.sqlite",
tableName: "app_store",
});
```
KV helpers return ready-to-use clients, so application code can import and call them directly. Mounting the clients is useful when you want one central configuration and a stable name that any server-side module can retrieve later.
## Register named mounts
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
import { appStorage } from "./src/lib/storage";
export default defineConfig({
storage: {
mounts: {
app: appStorage,
},
},
});
```
Each property under `mounts` is a KV namespace:
- `app` is an application-defined name. Farm does not attach special behavior to it.
- You can add names such as `cache`, `sessions`, `uploads`, or `webhooks` when the application needs more stores.
A mount name is not a URL, route, database table, or filesystem directory. It is the lookup name that connects configuration to later server-side calls.
`getStorage(name)` always returns a namespaced key/value view. When the name matches a configured mount, operations use that mount's driver. When no matching mount exists, Farm uses the same namespace on the root store instead. The default root store is in memory, so a missing or misspelled production mount does not throw but may store data only for the lifetime of one process. Rate-limit middleware is an exception: enforcement requires its dedicated atomic storage contract and never treats a generic KV mount as atomic.
## Use a mounted store
Call `getStorage("app")` from server-side code to retrieve the client registered under the `app` mount.
It is not limited to API routes. You can call it from any module that executes on the server after Farm initializes storage. Prefer resolving the store inside the request handler, action, job, or lifecycle function that uses it so the configured storage is ready before lookup.
**src/app/api/settings/route.ts**
```ts
import { createEndpoint } from "@farm.js/core/api";
import { getStorage } from "@farm.js/core/storage";
import { z } from "zod";
type AppSettings = {
theme: "light" | "dark";
productName: string;
};
const settingsSchema = z.object({
theme: z.enum(["light", "dark"]),
productName: z.string().min(1),
});
const defaultSettings: AppSettings = {
theme: "dark",
productName: "Farm.js App",
};
export const GET = createEndpoint("/api/settings", { method: "GET" }, async () => {
const appStore = getStorage("app");
const settings = await appStore.getItem("settings");
return {
settings: settings ?? defaultSettings,
};
});
export const POST = createEndpoint(
"/api/settings",
{
method: "POST",
body: settingsSchema,
},
async (ctx) => {
const appStore = getStorage("app");
await appStore.setItem("settings", ctx.body);
return {
saved: true,
settings: ctx.body,
};
},
);
```
The same mount can be used from other server-only application surfaces:
| Surface | Example use |
| ----------------------------------- | -------------------------------------------------------------------------------- |
| API routes | Persist settings, idempotency keys, webhook state, or cached provider responses. |
| Middleware | Read feature flags, request policy, tenant configuration, or custom counters. |
| Server components and pages | Load data needed during server rendering. |
| Server actions and server functions | Save form state or invalidate application-owned cached values. |
| Integration server handlers | Read application key/value data that is separate from an integration schema. |
| Jobs and workflows | Store checkpoints, deduplication markers, or lightweight progress state. |
Do not call `getStorage()` from a client component or browser bundle. Expose the required operation through an API route, server action, or server function instead.
## KV operations
Mounted stores expose the standard Farm KV API:
```ts
const appStore = getStorage("app");
await appStore.setItem("feature:checkout", { enabled: true });
const feature = await appStore.getItem<{ enabled: boolean }>("feature:checkout");
const exists = await appStore.hasItem("feature:checkout");
const keys = await appStore.getKeys("feature:");
await appStore.removeItem("feature:checkout");
await appStore.clear();
```
`clear()` only clears the selected namespace. Calling `getStorage("app").clear()` does not clear `ratelimit`, `cache`, or another mounted store.
Use descriptive keys such as `settings:global`, `tenant:acme:flags`, or `webhook:event_123`. Namespaced keys make inspection and targeted cleanup easier.
## Atomic rate-limit storage
The built-in rate limiter uses an atomic `increment(key, windowMs)` contract. Its default adapter is atomic inside one process, which is useful for local development and a single long-running server. Production deployments with multiple processes or regions should pass a shared atomic adapter explicitly:
```bash
pnpm add @farm.js/cache-redis ioredis
```
**src/app/api/middleware.ts**
```ts
import { redisRateLimitStorage } from "@farm.js/cache-redis";
import { middleware } from "@farm.js/core/middleware";
import Redis from "ioredis";
const rateLimits = redisRateLimitStorage({
client: () => new Redis(process.env.REDIS_URL!),
prefix: "storefront-ratelimit",
});
export default middleware().rateLimit({
requests: 100,
window: "1m",
storage: rateLimits,
});
```
The default bucket is the caller's address and the request path. A built app
takes that address from the server's own connection, as the development server
does. Behind a reverse proxy or a platform load balancer that connection belongs
to the proxy, so set `server.trustProxy` to key on the proxy's `x-forwarded-for`
instead. When the runtime reports no address at all, the limiter refuses rather
than putting every caller in one bucket, so set `trustProxy` or supply a
`keyGenerator` that uses an identity you do have. Do not fall back to a constant
such as `"unknown"`: that is the shared bucket the limiter refuses, and one
caller then exhausts the window for everyone.
The Redis adapter uses one Lua operation to increment the counter and establish its expiry. A generic `get()` followed by `set()` adapter is rejected because concurrent requests can read the same count and overwrite each other. Limited responses include `Retry-After`; all responses include the [`RateLimit` and `RateLimit-Policy` fields from the current IETF draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/).
Choose a trusted rate-limit identity when possible. An authenticated user or tenant ID is usually more useful than an IP address for application-level limits:
```ts
export default middleware().rateLimit({
requests: 20,
window: "1m",
keyGenerator: (ctx) => {
const userId = ctx.data.get("userId") as string | undefined;
if (userId) return `user:${userId}`;
const address = ctx.request.socket?.remoteAddress;
// No identity and no address means this request cannot be bucketed on its
// own, so reject it rather than sharing a bucket with every other caller.
if (!address) throw new Error("Rate limit identity is unavailable for this request.");
return `ip:${address}`;
},
});
```
Adapters may implement `get(key)` so `getRateLimitStatus()` can inspect a counter. Enforcement itself only depends on atomic `increment()`.
## Choosing mounts and drivers
Mount names describe the responsibility; drivers decide where the values live:
| Use case | Suggested mount | Typical driver |
| ------------------------------------------- | ---------------------------- | ----------------------------------------------------- |
| Application settings and durable JSON state | `app` or `settings` | SQLite, Postgres, MySQL, or libSQL |
| Shared cache entries | `cache` | Redis, Upstash Redis, or memory for local development |
| Rate-limit counters | Dedicated rate-limit adapter | `redisRateLimitStorage(...)` or another atomic store |
| Idempotency and webhook deduplication | `webhooks` or `idempotency` | Redis or a durable SQL store |
| Bucket-backed objects or metadata | `uploads` | S3 or Vercel Blob |
| Tests and disposable local state | Any descriptive name | Memory or local filesystem |
The mount names are conventions chosen by the application. Two mounts may use the same driver type while remaining isolated, or use different drivers based on durability and latency requirements. A mount named `ratelimit` is still generic KV storage and is not accepted as proof of atomic increment support.
## Supported KV drivers
Farm supports KV storage at three levels:
1. Farm convenience helpers for common databases, caches, and object stores.
2. Direct driver configuration through `driver: "name"`.
3. Existing or custom `unstorage` driver instances through `databaseStorage()`, `driverStorage()`, or `defineStorageClient()`.
The root store and every mount can use a different supported driver.
### Farm KV helpers
Import these helpers from `@farm.js/core/storage`:
| Helper | Driver | Common use |
| --------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------- |
| `memoryStorage()` | `memory` | Tests, local development, and disposable process-local state. |
| `localStorage({ base })` | Farm alias for `fs-lite` | Local persistent files, development caches, and self-hosted single-instance apps. |
| `sqliteStorage({ path, tableName })` | `sqlite` / `node-sqlite` | Durable local application state with no external service. |
| `postgresStorage(...)` / `pgStorage(...)` | `postgres` | Shared durable key/value state backed by Postgres. |
| `mysqlStorage(...)` / `mysql2Storage(...)` | `mysql` | Shared durable key/value state backed by MySQL. |
| `pgliteStorage(...)` | `pglite` | Embedded Postgres-compatible storage. |
| `planetscaleStorage(...)` | `planetscale` | PlanetScale-backed durable storage. |
| `libsqlStorage(...)` | `libsql` | Local or remote libSQL/Turso-compatible storage. |
| `redisStorage(...)` | `redis` | Shared caches, sessions, counters, and short-lived state. |
| `upstashStorage(...)` | `upstash` | HTTP-based Redis storage for serverless and edge-style deployments. |
| `mongodbStorage(...)` | `mongodb` | Durable document-backed key/value storage. |
| `s3Storage(...)` | `s3` | S3-compatible object-backed values. |
| `netlifyBlobsStorage(...)` | `netlify-blobs` | Named or deploy-scoped Netlify Blob stores. |
| `vercelKVStorage(...)` | `vercel-kv` | Vercel KV/Redis-backed shared state. |
| `vercelBlobStorage(...)` | `vercel-blob` | Public Vercel Blob-backed values. |
| `createStorageClient({ driver, ...options })` | Any supported name | Create a reusable client from direct driver configuration. |
| `databaseStorage(database, { tableName })` | `db0` | Reuse an existing `db0` database instance. |
| `driverStorage(driver)` | Custom | Wrap an existing `unstorage` driver or driver factory. |
| `defineStorageClient(factory)` | Custom | Lazily create a custom synchronous or asynchronous driver. |
### Configure drivers directly
Helpers and direct configuration produce the same Farm storage client behavior. Driver options can be written inline or inside `options`. If both are present, values inside `options` take precedence.
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
storage: {
driver: "sqlite",
path: "./.farm/storage/root.sqlite",
tableName: "farm_root",
mounts: {
cache: {
driver: "redis",
url: process.env.REDIS_URL!,
ttl: 300,
},
uploads: {
driver: "s3",
endpoint: "https://s3.us-east-1.amazonaws.com",
region: "us-east-1",
bucket: process.env.S3_BUCKET!,
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
},
},
});
```
In this example, `getStorage()` uses SQLite, `getStorage("cache")` uses Redis, and `getStorage("uploads")` uses S3.
### Database-backed KV drivers
Farm resolves these names through `db0` and exposes the result as key/value storage:
| Accepted driver names | Database connector | Important configuration |
| ------------------------------ | ----------------------- | ----------------------------------------------------------------- |
| `sqlite`, `node-sqlite` | Node SQLite | `path` or `name`, plus optional `tableName`. |
| `sqlite3` | `sqlite3` | `path` or `name`, plus optional `tableName`. |
| `better-sqlite3` | `better-sqlite3` | `path` or `name`, plus optional `tableName`. |
| `postgres`, `postgresql`, `pg` | PostgreSQL | `url` or standard `pg` client options, plus optional `tableName`. |
| `mysql`, `mysql2` | MySQL | Standard `mysql2` connection options, plus optional `tableName`. |
| `pglite` | PGlite | PGlite connector options, plus optional `tableName`. |
| `planetscale` | PlanetScale | PlanetScale client options, plus optional `tableName`. |
| `libsql`, `libsql-node` | libSQL Node client | `url`, optional `authToken`, and optional `tableName`. |
| `libsql-http` | libSQL HTTP client | `url`, optional `authToken`, and optional `tableName`. |
| `db0` | Existing `db0` database | Pass `{ database, tableName? }` inside `options`. |
These drivers create or use a key/value table for Farm KV storage. They do not expose tables, models, joins, or arbitrary SQL through `getStorage()`. Passing a raw database or ORM object through `storage.client` is a separate integration database path.
### Complete built-in driver set
Farm also accepts the built-in driver names exported by its installed `unstorage` version. Kebab-case and camel-case names shown on the same row are aliases.
| Category | Accepted driver names | Intended use |
| ----------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Process memory | `memory` | Fast, process-local, non-durable state. |
| Disabled storage | `null` | Discard writes and always read empty state. Useful for explicitly disabling a store. |
| Layered storage | `overlay` | Combine multiple driver layers. Requires configured driver instances in `layers`. |
| Bounded memory | `lru-cache`, `lruCache` | Process-local cache with size and TTL controls. |
| Filesystem | `local`, `fs-lite`, `fsLite`, `fs` | Persistent files. `local` is Farm's shorthand for `fs-lite`; `fs` adds watcher support. |
| Remote HTTP | `http` | Read and write through an HTTP storage endpoint. |
| GitHub content | `github` | Read-only, cached repository content access. |
| Redis | `redis` | Shared cache, counters, sessions, and short-lived state. |
| Upstash Redis | `upstash` | REST-based Redis for serverless runtimes. |
| MongoDB | `mongodb` | MongoDB collection-backed values. |
| S3-compatible objects | `s3` | AWS S3, Cloudflare R2's S3 API, and compatible providers. |
| UploadThing | `uploadthing` | UploadThing-backed object values. |
| Netlify | `netlify-blobs`, `netlifyBlobs` | Named or deploy-scoped Netlify Blob storage. |
| Vercel KV | `vercel-kv`, `vercelKV` | Vercel-managed Redis-compatible key/value storage. |
| Vercel Blob | `vercel-blob`, `vercelBlob` | Vercel Blob object storage. |
| Vercel Runtime Cache | `vercel-runtime-cache`, `vercelRuntimeCache` | Ephemeral regional runtime cache with TTL and tags. |
| Cloudflare KV binding | `cloudflare-kv-binding`, `cloudflareKVBinding` | A KV namespace bound to a Cloudflare runtime. |
| Cloudflare KV HTTP | `cloudflare-kv-http`, `cloudflareKVHttp` | Cloudflare KV through the REST API. |
| Cloudflare R2 binding | `cloudflare-r2-binding`, `cloudflareR2Binding` | An R2 bucket bound to a Cloudflare runtime. |
| Azure App Configuration | `azure-app-configuration`, `azureAppConfiguration` | Azure App Configuration key/value data. |
| Azure Cosmos DB | `azure-cosmos`, `azureCosmos` | Cosmos DB container-backed values. |
| Azure Key Vault | `azure-key-vault`, `azureKeyVault` | Secret-backed values in Azure Key Vault. |
| Azure Blob Storage | `azure-storage-blob`, `azureStorageBlob` | Azure container-backed object values. |
| Azure Table Storage | `azure-storage-table`, `azureStorageTable` | Azure table-backed key/value state. |
| Deno KV | `deno-kv`, `denoKV` | Deno runtime KV storage. |
| Deno KV from Node | `deno-kv-node`, `denoKVNode` | Deno KV through the Node-compatible client. |
| IndexedDB | `indexedb` | IndexedDB-backed values in a compatible browser runtime. |
| Web Storage | `localstorage`, `session-storage`, `sessionStorage` | Browser local or session storage. |
| Capacitor | `capacitor-preferences`, `capacitorPreferences` | Capacitor Preferences-backed mobile storage. |
Farm KV storage normally initializes on the server. Browser-only drivers require a compatible custom runtime and should not be used as a reason to call `getStorage()` from client components.
Most remote and platform drivers load an optional provider SDK. Install the package required by the selected driver, such as `ioredis`, `mongodb`, `@upstash/redis`, `@vercel/kv`, `@vercel/blob`, `@netlify/blobs`, `aws4fetch`, `uploadthing`, the relevant Azure SDK, `@deno/kv`, `idb-keyval`, or `lru-cache`. Database aliases may likewise require `sqlite3`, `better-sqlite3`, `mysql2`, `@electric-sql/pglite`, `@planetscale/database`, or `@libsql/client`; the `sqlite` alias uses Node's built-in `node:sqlite`. Cloudflare binding drivers instead require the corresponding runtime binding.
### Custom drivers
Use `driverStorage()` when a compatible driver already exists:
```ts
import { driverStorage } from "@farm.js/core/storage";
import createCustomDriver from "my-unstorage-driver";
export const customStorage = driverStorage(() =>
createCustomDriver({
endpoint: process.env.CUSTOM_STORAGE_URL!,
}),
);
```
The wrapped driver can be mounted or used as the root Farm storage client just like a built-in helper.
## Database and ORM clients are separate
The current beta API uses `storage.client` for two distinguishable object shapes:
- A Farm storage client, such as `sqliteStorage(...)`, becomes the root key/value store used by `getStorage()`.
- A raw database, ORM, or provider object becomes the runtime client for schema-backed integrations. That object is not returned by `getStorage(name)`.
```ts
import { defineConfig } from "@farm.js/core";
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("farm.sqlite");
export default defineConfig({
storage: {
client: db,
},
});
```
In this example, the raw SQLite database is available only to integrations. Because no KV driver or Farm storage client is configured, `getStorage()` continues to use the default in-memory KV store.
When an integration also defines a schema, Farm exposes a typed ORM layer at `ctx.args.db`. The integration does not need to know whether the app passed SQLite, PostgreSQL, or another supported runtime client.
Use this rule of thumb:
- Use `getStorage("name")` for key/value operations against a named namespace or mount.
- Use `ctx.args.db` inside an integration that declares models and needs ORM-style queries.
- Use `ctx.args.storage.getClient()` inside integration server code only when provider-specific operations require the raw configured runtime client.
See [Database and ORM Clients](/docs/integrations/orm-storage) for application-owned ORM usage, integration schemas, PostgreSQL pools, Better Auth ownership, and migrations.
## KV client or database client
| Config | Use it for |
| ------------------------------------------- | ------------------------------------------------------ |
| `sqliteStorage(...)` | Farm KV storage with a Farm storage client. |
| `redisStorage(...)` | Cache or queue-like key/value data. |
| `storage.mounts` | Multiple named key/value stores. |
| `storage.client` with a Farm storage client | Reuse a created Farm storage client as the root store. |
| `storage.client` with a DB/runtime object | Give integrations a runtime database client. |
## Production notes
- Use a shared durable KV driver for production state that must survive restarts or be visible across instances.
- Keep memory KV storage for tests and explicitly disposable local state.
- Do not assume that a PostgreSQL-backed KV helper provides relational database access.
- Configure KV mounts and database clients independently when an application needs both.
---
## Product telemetry
URL: /docs/telemetry
Understand and control Farm.js CLI and production-site telemetry.
# Product telemetry
Farm.js has two separately controlled product-telemetry paths:
- Anonymous command telemetry in `@farm.js/cli` (`farm`) and `@farm.js/create-app` is **enabled by
default** for interactive local commands.
- Production website reporting is **enabled by default** in built server runtimes and automatically
reduces incoming production request URLs to their public HTTPS origin.
These signals help the maintainers understand which coarse framework paths are useful, where
compatibility work should be focused, and which websites are running Farm in production.
This is separate from [application observability](/docs/observability). OpenTelemetry describes what
your application does and is configured by the application owner. Farm.js product telemetry never
includes application spans, logs, visitor analytics, or request contents.
## Control CLI telemetry
```bash
farm telemetry status
farm telemetry enable
farm telemetry disable
```
The first eligible event creates a random anonymous installation ID in the operating system's local
configuration directory. `farm telemetry disable` opts out and deletes that ID. Running
`farm telemetry enable` later creates a different ID. Saved opt-out preferences remain respected
across upgrades.
Environment variables can provide an explicit per-process or organization-wide policy:
| Variable | Behavior |
| --------------------------- | ----------------------------------------------------------------------- |
| `FARM_TELEMETRY=1` | Enables CLI telemetry, including non-interactive and CI commands. |
| `FARM_TELEMETRY=0` | Disables CLI and production-site telemetry for the process. |
| `FARM_TELEMETRY_DISABLED=1` | Disables CLI and production-site telemetry for the process. |
| `DO_NOT_TRACK=1` | Disables all telemetry and takes precedence over Farm's enable setting. |
| `FARM_TELEMETRY_DEBUG=1` | Prints delivery status without event contents, origins, or identifiers. |
Without the explicit `FARM_TELEMETRY=1` override, Farm skips test, CI, and non-interactive
processes even when the saved local preference is enabled.
The preference file is stored at:
- macOS: `~/Library/Application Support/farmjs/telemetry.json`
- Linux: `$XDG_CONFIG_HOME/farmjs/telemetry.json`, or `~/.config/farmjs/telemetry.json`
- Windows: `%APPDATA%\farmjs\telemetry.json`
## Automatic production-site detection
No site URL configuration is required. After the first non-health request reaches a built Farm
server runtime, Farm reads the request URL and reduces it to an origin. For example,
`https://shop.example.com/private/orders?token=secret` becomes only
`https://shop.example.com` before telemetry delivery.
Farm accepts detected origins only when they use HTTPS and a public hostname. HTTP, localhost, IP,
single-label, and `.local` origins are ignored. Request paths, query strings, hashes, and credentials
are discarded and never included in the check-in.
To disable production-site reporting in `farm.config.ts`:
```ts title="farm.config.ts"
import { defineConfig } from "@farm.js/core";
export default defineConfig({
telemetry: false,
});
```
Vercel preview, development, and custom-environment deployments are skipped automatically from
`VERCEL_ENV` and `VERCEL_TARGET_ENV`, as are Netlify preview, branch, and dev deployments (when
`NETLIFY=true`) and Render pull-request previews (when `IS_PULL_REQUEST=true`). Farm does not
classify deployments from the hostname suffix, so a production website whose public domain ends
in `.vercel.app` remains eligible. On a provider Farm does not auto-detect, set
`FARM_TELEMETRY=0` or `FARM_TELEMETRY_DISABLED=1` in preview environments while leaving
production enabled.
The Farm-owned endpoint removes the Vercel-confirmed legacy preview aliases that were stored before
this runtime guard existed. It does not guess whether a deployment is a preview from its hostname.
After the first non-health production request, Farm schedules a check-in through the deployment
runtime's background-work hook. It does not wait for the network before handling or returning the
application response. A running instance checks in at most once every 24 hours, and a failed request
is eligible for a later best-effort retry. Multiple instances update the same site record.
The check-in contains only the detected origin, `@farm.js/core` version, renderer name, and deploy
target. It does not contain a visitor or installation identifier, the full request URL beyond the
reported origin, headers, cookies, IP address, user-agent string, or application data. Fully static
exports have no server runtime and therefore do not send production-site check-ins. Vercel preview,
development, and custom-environment deployments do not send production-site check-ins.
Set `telemetry: false` and redeploy to stop future check-ins. An inactive site disappears from the
maintainer dashboard after the retention window.
## Data that is sent
Farm currently sends three versioned event types:
| Event | Fields |
| ------------------------ | ------------------------------------------------------------------------------------------------------- |
| `command_invoked` | Allowlisted command from `farm` or `create-farm-app`, package/version, optional deploy target, runtime. |
| `project_created` | Allowlisted starter, renderer, package manager, TypeScript/install booleans, runtime fields. |
| `production_site_active` | Automatically detected HTTPS origin, core version, renderer, and deploy target. |
The `farm` binary records each actionable command path, including nested commands such as
`auth:migrate`, `cron:list`, `cron:run`, and `add:integration`. The app generator records `create`
or `list-templates`, and a completed scaffold also records `project_created`. Help, version, and the
`farm telemetry` privacy-control commands do not emit events.
Every CLI event also has a random event ID for deduplication and the random local installation ID.
The server immediately converts the installation ID into an HMAC hash using a server-only salt;
the raw ID is not stored. Production-site check-ins have neither identifier and are upserted by the
detected origin. A telemetry-enabled production server also answers
`/.well-known/farm-telemetry` with the same framework metadata so the ingestion service can attest
the claimed origin before storage. Receipt time is assigned by the server instead of trusting a
client timestamp.
Farm does **not** collect or store:
- project names, filesystem paths, Git remotes, repository names, source code, or route names;
- usernames, email addresses, account IDs, cookies, application payloads, or application events;
- environment variable names or values, database URLs, credentials, tokens, or other secrets;
- IP addresses or user-agent strings.
The CLI client schedules delivery in the background so telemetry does not delay command execution,
and network delivery does not keep a short-lived process open. A request has a three-second timeout
and transient network, rate-limit, and server failures receive two bounded retries while the process
remains alive. Telemetry can never make a Farm command fail, and there is no persistent retry queue.
## Endpoint, validation, and retention
CLI events are posted to `https://farmjs.dev/api/telemetry/v1/events`; production-site check-ins are
posted to `https://farmjs.dev/api/telemetry/v1/sites`. Both endpoints accept a strict, versioned JSON
schema, reject unknown fields and bodies larger than 8 KiB, and rate-limit traffic. CLI events are
deduplicated by event ID. Before a production site is upserted, the service fetches its fixed
well-known endpoint through a private-address-blocking connection, rejects redirects and oversized
responses, and stores the metadata returned by that origin instead of client-claimed metadata.
Rows created before origin attestation was introduced remain conservatively labeled as unverified
and disappear through the normal retention window unless a current deployment attests them.
Raw telemetry events and inactive production-site records are retained for 90 days by default and
are pruned by the ingestion service. The verified legacy Vercel preview records are also removed
during this maintenance pass. Aggregated package-download counts remain available independently
through npm's public download statistics. A deployment operator can change the retention window
with `FARM_TELEMETRY_RETENTION_DAYS`.
For local endpoint development only, `FARM_TELEMETRY_ENDPOINT` and
`FARM_TELEMETRY_SITE_ENDPOINT` can point at an HTTPS URL or an HTTP localhost address. Released
clients use the Farm-owned endpoints by default. An invalid or insecure explicit override is skipped
instead of being redirected to a Farm-owned endpoint.
## Maintainer deployment setup
The Farm-owned docs deployment uses four server-only environment variables:
| Variable | Purpose |
| -------------------------------- | -------------------------------------------------------------------- |
| `DATABASE_URL` | Pooled Postgres connection used by Prisma. |
| `FARM_TELEMETRY_IDENTITY_SALT` | Long random secret used to HMAC-hash local anonymous IDs. |
| `FARM_TELEMETRY_DASHBOARD_TOKEN` | Long random secret used to open the internal `/telemetry` dashboard. |
| `FARM_TELEMETRY_RETENTION_DAYS` | Optional event and inactive-site retention window; defaults to `90`. |
These values must be encrypted deployment variables and must never use a `PUBLIC_` prefix or be
committed to the repository. Public telemetry clients do not contain an ingestion secret; the
endpoints use strict validation, body limits, rate limits, event deduplication, and origin
attestation instead.
After connecting Postgres, generate the Prisma client and apply the schema from the repository:
```bash
pnpm --dir docs prisma:generate
pnpm --dir docs db:push
```
`/telemetry` exchanges the dashboard token through a server-side form for a 12-hour HttpOnly,
SameSite cookie. The token is not placed in the URL, local storage, or client-side JavaScript.
---
## Testing
URL: /docs/testing
Test Farm requests, programmatic routes, API endpoints, and server functions with the real framework runtimes.
# Testing
Farm provides runner-agnostic helpers from `@farm.js/core/testing`. They use Web `Request` and `Response` objects and call the same route data, API endpoint, and server-function runtimes used by the application.
## Create a harness
Set request defaults once for a test file. The `context` callback has the same input and behavior as `context` in `farm.config.ts`.
```ts
import { afterEach } from "vitest";
import { createFarmTestHarness } from "@farm.js/core/testing";
import { getSession } from "../src/session";
export const farm = createFarmTestHarness({
origin: "https://app.example.test",
headers: { "x-tenant": "acme" },
cookies: { session: "test-session" },
context: async ({ request }) => ({
session: await getSession(request),
}),
});
afterEach(() => {
farm.clearCache();
});
```
Harness defaults are merged with each call. Per-call headers and cookies win when the same name is provided.
## Test a route
Pass the exported `createRoute` value directly. Farm builds the URL, validates params and search, resolves request context, runs the guard, then runs `data.before`, `data.main`, and `data.after` in production order.
```ts
import { expect, test } from "vitest";
import { ProductRoute } from "../src/features/products/page";
import { farm } from "./farm";
test("loads a product route", async () => {
const result = await farm.route(ProductRoute, {
params: { id: "product-1" },
search: { tab: "reviews" },
});
expect(result.request.url).toBe("https://app.example.test/products/product-1?tab=reviews");
expect(result.props.data.product.id).toBe("product-1");
expect(result.props.search.tab).toBe("reviews");
});
```
`result.props` preserves the route's inferred params, search, and `data.main` result types. It is the safe prop object passed to the page component: private app context is not included. `result.element` is a React element for the route component, while `result.canonicalPath` exposes search cleanup such as stripped defaults or temporary params.
Guards keep their normal control flow, so redirects and not-found signals can be asserted directly:
```ts
await expect(
farm.route(DashboardRoute, {
context: { session: { user: null } },
}),
).rejects.toMatchObject({
digest: "FARM_REDIRECT;307;/login",
});
```
Pass `context` to one route call to override the harness context factory. Use `pluginContext` or `middleware` when the component also needs request-scoped plugin or middleware data.
## Test API routes
Use `api` for a programmatic API route. Dynamic params are interpolated and then matched again by the real API matcher. Endpoint schemas and response normalization still run.
```ts
import { expect, test } from "vitest";
import { ProjectApi } from "../src/features/projects/api";
import { farm } from "./farm";
test("updates a project", async () => {
const response = await farm.api(ProjectApi, {
method: "PATCH",
params: { id: "project-1" },
query: { source: "form" },
json: { name: "Farm" },
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
id: "project-1",
name: "Farm",
});
});
```
Use `endpoint` for a handler exported by a file route:
```ts
import { GET } from "../src/app/api/projects/[id]/route";
const response = await farm.endpoint(GET, {
path: "/api/projects/project-1",
params: { id: "project-1" },
});
```
The helpers return JSON 404 and 405 responses just like the API route manager. Thrown handlers become a 500 response by default. Set `throwOnError: true` when a unit test needs to inspect the original error.
## Test server functions
`serverFn` keeps input and return inference, including Zod validation and `FormData` conversion. It also installs the request and cancellation signal that the handler reads. Any `createServerMiddleware` dependencies run through the same chain as production, including their typed context, ordering, and errors.
```ts
import { expect, test } from "vitest";
import { signup } from "../src/actions/signup";
import { farm } from "./farm";
test("signs up a user", async () => {
const result = await farm.serverFn(signup, {
email: "ada@example.com",
password: "correct horse battery staple",
});
expect(result.ok).toBe(true);
});
```
Pass a `FormData` value to test form actions, or provide an exact request when the handler reads authorization headers or cookies:
```ts
const request = farm.request("/actions/signup", {
method: "POST",
headers: { authorization: "Bearer test-token" },
});
await farm.serverFn(signup, formData, { request });
```
Use an aborted signal to test cancellation:
```ts
const controller = new AbortController();
controller.abort(new Error("cancelled"));
await expect(farm.serverFn(signup, input, { signal: controller.signal })).rejects.toThrow(
"cancelled",
);
```
## Build requests
Use `createTestRequest` without a harness, or `farm.request` when you want harness defaults.
```ts
import { createTestRequest } from "@farm.js/core/testing";
const request = createTestRequest("/api/search", {
origin: "https://app.example.test",
query: { q: "routing", tag: ["farm", "react"] },
cookies: { preview: "enabled" },
json: { limit: 10 },
});
```
Provide only one of `json`, `form`, or `body`. A body defaults the method to `POST`; explicit `GET` and `HEAD` requests reject non-empty bodies.
## Testing boundaries
- Clear the route data cache in `afterEach` when tests use `data.key`. The cache is process-wide, matching the application runtime.
- Keep database, storage, email, and provider clients behind context or module boundaries so tests can supply controlled implementations.
- Use `farm.route` for route contracts and hook flow. Use a React renderer when the assertion concerns component interactions or DOM output.
- Use `farm.serverFn` for input validation, handler authorization, form conversion, request access, and cancellation.
- Add an HTTP E2E test for server-action transport security. Direct server-function tests do not exercise Origin/Host checks, Fetch Metadata, action-reference decoding, or payload-size limits.
- Assert both success and expected failures. A typed result such as `{ ok: false, reason: "forbidden" }` should be tested separately from unexpected thrown errors.
---
## Themes
URL: /docs/themes
Configure light, dark, and system color modes with a pre-paint selector, Tailwind variants, and typed client and server APIs.
# Themes
FARMJS can manage a visitor's light, dark, or system preference without owning your colors. The
framework applies the active mode before styles load, persists the preference, and exposes typed
client and server APIs. Tailwind utilities, CSS variables, or ordinary selectors still define how
the application looks.
## Enable themes
Add `theme` to the application config:
**farm.config.ts**
```ts
import { defineConfig } from "@farm.js/core";
export default defineConfig({
theme: {
default: "system",
storageKey: "farm-theme",
},
});
```
`default` accepts `"light"`, `"dark"`, or `"system"`. Theme support is opt-in; omit the property or
set it to `false` when the application manages color mode itself.
FARMJS stores the preference in a same-site cookie so server rendering can read it. It mirrors
changes to local storage for cross-tab updates. The cookie path follows the configured `basePath`.
## Tailwind dark variants
When the built-in Tailwind integration processes a stylesheet containing `@import "tailwindcss"`,
FARMJS connects the `dark:` variant to its `data-theme` selector automatically:
```tsx
export function Panel() {
return (
Theme-aware content
);
}
```
The active document is either `` or ``. If the
stylesheet already defines `@custom-variant dark`, FARMJS preserves that definition instead of
overriding it.
## Plain CSS and design tokens
Tailwind is optional. Use the same selector with CSS variables or ordinary styles:
```css
:root {
--background: #ffffff;
--foreground: #0a0a0a;
}
[data-theme="dark"] {
--background: #000000;
--foreground: #f5f5f5;
}
body {
background: var(--background);
color: var(--foreground);
}
```
FARMJS also sets `color-scheme` for the resolved mode so native form controls and browser surfaces
match the page.
## Read and change the theme
Use `useTheme` in a client component:
```tsx
"use client";
import { useTheme } from "@farm.js/core/theme/client";
export function ThemePicker() {
const { theme, resolvedTheme, mounted, setTheme } = useTheme();
return (
);
}
```
The returned values have different jobs:
- `theme` is the saved `"light"`, `"dark"`, or `"system"` preference.
- `resolvedTheme` is the active `"light"` or `"dark"` browser mode. It is undefined during server
rendering when the saved preference is `"system"`.
- `mounted` becomes true when the browser runtime is active.
- `setTheme(theme)` saves and applies a preference.
- `toggleTheme()` switches between the resolved light and dark modes.
The client module also exports non-hook `getTheme`, `setTheme`, and `toggleTheme` functions for
event handlers or stores outside React components.
## Read the preference on the server
Server-rendered pages and helpers can read the cookie-backed preference:
```tsx
import { getTheme } from "@farm.js/core/theme/server";
export default function SettingsPage() {
const theme = getTheme();
return
Saved preference: {theme}
;
}
```
`getTheme()` returns the preference, not an invented server-side resolution for `"system"`; only
the browser knows the visitor's operating-system color mode. Prefer CSS and `useTheme` for visual
styling. When server-rendered content depends on the cookie, treat that route as request-specific
rather than shared static output.
## No-flash behavior
FARMJS places a small bootstrap script and color-scheme style at the start of the document head.
They resolve the cookie and operating-system preference before application CSS loads. The runtime
also follows operating-system changes while `theme === "system"` and preserves `data-theme` during
SPA document navigation.
## Configuration reference
| Option | Type | Default | Purpose |
| ------------ | ------------------------------- | -------------- | --------------------------------------------- |
| `default` | `"light" \| "dark" \| "system"` | `"system"` | Preference used before a visitor chooses one. |
| `storageKey` | `string` | `"farm-theme"` | Cookie and cross-tab storage key. |
---
## Upgrading to 0.1
URL: /docs/upgrading
Move a Farm app from the 0.1.0 betas to the stable 0.1 release: upgrade commands, behavior changes, and deprecated APIs to replace before 0.2.
# Upgrading to 0.1
Farm 0.1.0 is the first stable release. It follows `0.1.0-beta.108` and carries the
[stability policy](/docs/stability): patch releases keep stable APIs working, and a minor release
removes a stable API only after deprecating it first.
## Upgrade the packages
Run the upgrade command from the app root:
```bash
farm upgrade --latest
```
It moves every published `@farm.js/*` dependency to its `latest` tag with the app's package
manager. Packages in the shared release group, such as `@farm.js/core`, `@farm.js/cli`,
`@farm.js/devtools`, and the integrations, move to `0.1.0`. Renderer packages and plugins that are
still in beta move to their newest beta, which is what their `latest` tag points to. Preview the
commands first with `farm upgrade --latest --dry-run`.
To upgrade by hand, drop the `@beta` tag and keep every shared-group package on the same version.
The integration packages pin `@farm.js/core` exactly, so mixing versions produces peer conflicts:
```bash
pnpm add @farm.js/core@latest
pnpm add -D @farm.js/cli@latest @farm.js/devtools@latest
```
pnpm 10.16 and newer can hold back versions published in the last day through
`minimumReleaseAge`. Templates add `@farm.js/*` to `minimumReleaseAgeExclude`; add the same entry
to your `pnpm-workspace.yaml` if an upgrade right after a release resolves an older version.
## Behavior changes
These changes landed between the last beta and 0.1.0. Most apps need no code change.
- **React streaming on Node targets.** With React 19, production servers on Node presets now stream
through `renderToPipeableStream`, the same primitive development uses. Earlier betas used React's
Web stream on Node when it was available. Response bodies are unchanged.
- **React on edge targets.** Edge presets bundle React DOM's edge server build (`server.browser` on
React 18) instead of its Node build. Cloudflare module Workers now bundle React instead of
failing at startup with `No such module "react"`.
- **Content Security Policy nonces and hashes.** Enable `security.csp.nonce` for a strict script
policy. Farm generates a per-request nonce for dynamic HTML and applies it to streamed script
elements. Fully prerendered pages stay static and receive per-page SHA-256 hashes instead. See
[Content Security Policy](/docs/configuration#content-security-policy).
- **Caller-owned databases.** `storage.dispose()` no longer closes a database you passed to
`databaseStorage(database)`. Databases Farm creates from storage configuration are still closed
by Farm.
- **DevTools version.** `@farm.js/devtools` is now released with core and shares its version.
## Deprecated APIs to replace before 0.2
These still work in 0.1 and are scheduled for removal in a later minor release.
### Plugin lifecycle hooks
| Deprecated | Use instead |
| --------------------------------------------------------------- | -------------------------------- |
| `init` | `setup` |
| `config` | `configure` |
| `ready` | `runtime.start` |
| `shutdown` | `runtime.close` |
| `devServerCreated` | `dev.server` |
| `hmrUpdate` | `dev.update` |
| `buildStart`, `beforeBundle` | `build.before` |
| `buildEnd`, `afterBundle` | `build.after` |
| `beforeNitroBuild` | `build.configure` |
| `routeDiscovered`, `middlewareDiscovered`, `apiRouteDiscovered` | `router.discovered` |
| `routesGenerated` | `router.generated` |
| `beforeRouteMatch` / `afterRouteMatch` | `router.before` / `router.after` |
| `beforeRender` | `render.before` |
| `afterRender`, `transformHTML` | `render.html` |
| `transformPage` | `render.before` or `render.html` |
| `beforeApiHandler`, `beforeRequest` | `runtime.before` |
| `afterApiHandler`, `afterResponse` | `runtime.after` |
| `requestContext` on the plugin context | `ctx.req` |
### Integrations and schemas
| Deprecated | Use instead |
| -------------------------------------------------------- | ------------------------------------- |
| `slot` on integration descriptors, `FarmIntegrationSlot` | `category`, `FarmIntegrationCategory` |
| `requestContext`, `FarmIntegrationRequestContextStore` | `req`, `FarmRequestStore` |
| `defineIntegrationSchema` | `defineSchema` (an exact alias) |
| `FarmIntegrationSchema*` types | the matching `FarmSchema*` types |
### Configuration and routes
| Deprecated | Use instead |
| -------------------------------------------- | ---------------------------------------------------- |
| The built-in DevTools dashboard (`devtools`) | `devtools()` from `@farm.js/devtools` in `plugins` |
| `defineCron()` | `cron` in `farm.config.ts`, pointing at an API route |
| `generateStaticParams` | `getStaticPaths` |
| `md.routes` | `md.expose` |
| `security.contentSecurityPolicy` | `security.csp` |
## Getting help
If an app behaves differently after upgrading, run `farm doctor` and open an issue with its output,
the renderer, and the deployment target.
---