Farm.js

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 needUse
Cache entries, flags, settings, counters, or JSONFarm KV storage through getStorage(name)
Rate limiting shared across production instancesAn atomic adapter such as redisRateLimitStorage(...)
Files or values addressed by one keyAn object-backed KV helper such as s3Storage(...)
Application models, relations, joins, and filtersAn application-owned @farming-labs/orm or another ORM
Models owned by a schema-backed Farm integrationFarm's integration ORM through ctx.args.db
Better Auth users, accounts, and sessionsBetter Auth's configured database adapter and instance APIs
Provider-specific SQL or database operationsThe 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.

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
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
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
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<AppSettings>("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:

SurfaceExample use
API routesPersist settings, idempotency keys, webhook state, or cached provider responses.
MiddlewareRead feature flags, request policy, tenant configuration, or custom counters.
Server components and pagesLoad data needed during server rendering.
Server actions and server functionsSave form state or invalidate application-owned cached values.
Integration server handlersRead application key/value data that is separate from an integration schema.
Jobs and workflowsStore 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:

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:

pnpm add @farm.js/cache-redis ioredis
src/app/api/middleware.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,
  keyGenerator: (ctx) => {
    return ctx.request.socket.remoteAddress ?? "unknown";
  },
});

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.

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:

export default middleware().rateLimit({
  requests: 20,
  window: "1m",
  keyGenerator: (ctx) => {
    const userId = ctx.data.get("userId") as string | undefined;
    return userId ? `user:${userId}` : `ip:${ctx.request.socket.remoteAddress ?? "unknown"}`;
  },
});

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 caseSuggested mountTypical driver
Application settings and durable JSON stateapp or settingsSQLite, Postgres, MySQL, or libSQL
Shared cache entriescacheRedis, Upstash Redis, or memory for local development
Rate-limit countersDedicated rate-limit adapterredisRateLimitStorage(...) or another atomic store
Idempotency and webhook deduplicationwebhooks or idempotencyRedis or a durable SQL store
Bucket-backed objects or metadatauploadsS3 or Vercel Blob
Tests and disposable local stateAny descriptive nameMemory 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:

HelperDriverCommon use
memoryStorage()memoryTests, local development, and disposable process-local state.
localStorage({ base })Farm alias for fs-liteLocal persistent files, development caches, and self-hosted single-instance apps.
sqliteStorage({ path, tableName })sqlite / node-sqliteDurable local application state with no external service.
postgresStorage(...) / pgStorage(...)postgresShared durable key/value state backed by Postgres.
mysqlStorage(...) / mysql2Storage(...)mysqlShared durable key/value state backed by MySQL.
pgliteStorage(...)pgliteEmbedded Postgres-compatible storage.
planetscaleStorage(...)planetscalePlanetScale-backed durable storage.
libsqlStorage(...)libsqlLocal or remote libSQL/Turso-compatible storage.
redisStorage(...)redisShared caches, sessions, counters, and short-lived state.
upstashStorage(...)upstashHTTP-based Redis storage for serverless and edge-style deployments.
mongodbStorage(...)mongodbDurable document-backed key/value storage.
s3Storage(...)s3S3-compatible object-backed values.
netlifyBlobsStorage(...)netlify-blobsNamed or deploy-scoped Netlify Blob stores.
vercelKVStorage(...)vercel-kvVercel KV/Redis-backed shared state.
vercelBlobStorage(...)vercel-blobPublic Vercel Blob-backed values.
createStorageClient({ driver, ...options })Any supported nameCreate a reusable client from direct driver configuration.
databaseStorage(database, { tableName })db0Reuse an existing db0 database instance.
driverStorage(driver)CustomWrap an existing unstorage driver or driver factory.
defineStorageClient(factory)CustomLazily 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
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 namesDatabase connectorImportant configuration
sqlite, node-sqliteNode SQLitepath or name, plus optional tableName.
sqlite3sqlite3path or name, plus optional tableName.
better-sqlite3better-sqlite3path or name, plus optional tableName.
postgres, postgresql, pgPostgreSQLurl or standard pg client options, plus optional tableName.
mysql, mysql2MySQLStandard mysql2 connection options, plus optional tableName.
pglitePGlitePGlite connector options, plus optional tableName.
planetscalePlanetScalePlanetScale client options, plus optional tableName.
libsql, libsql-nodelibSQL Node clienturl, optional authToken, and optional tableName.
libsql-httplibSQL HTTP clienturl, optional authToken, and optional tableName.
db0Existing db0 databasePass { 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.

CategoryAccepted driver namesIntended use
Process memorymemoryFast, process-local, non-durable state.
Disabled storagenullDiscard writes and always read empty state. Useful for explicitly disabling a store.
Layered storageoverlayCombine multiple driver layers. Requires configured driver instances in layers.
Bounded memorylru-cache, lruCacheProcess-local cache with size and TTL controls.
Filesystemlocal, fs-lite, fsLite, fsPersistent files. local is Farm's shorthand for fs-lite; fs adds watcher support.
Remote HTTPhttpRead and write through an HTTP storage endpoint.
GitHub contentgithubRead-only, cached repository content access.
RedisredisShared cache, counters, sessions, and short-lived state.
Upstash RedisupstashREST-based Redis for serverless runtimes.
MongoDBmongodbMongoDB collection-backed values.
S3-compatible objectss3AWS S3, Cloudflare R2's S3 API, and compatible providers.
UploadThinguploadthingUploadThing-backed object values.
Netlifynetlify-blobs, netlifyBlobsNamed or deploy-scoped Netlify Blob storage.
Vercel KVvercel-kv, vercelKVVercel-managed Redis-compatible key/value storage.
Vercel Blobvercel-blob, vercelBlobVercel Blob object storage.
Vercel Runtime Cachevercel-runtime-cache, vercelRuntimeCacheEphemeral regional runtime cache with TTL and tags.
Cloudflare KV bindingcloudflare-kv-binding, cloudflareKVBindingA KV namespace bound to a Cloudflare runtime.
Cloudflare KV HTTPcloudflare-kv-http, cloudflareKVHttpCloudflare KV through the REST API.
Cloudflare R2 bindingcloudflare-r2-binding, cloudflareR2BindingAn R2 bucket bound to a Cloudflare runtime.
Azure App Configurationazure-app-configuration, azureAppConfigurationAzure App Configuration key/value data.
Azure Cosmos DBazure-cosmos, azureCosmosCosmos DB container-backed values.
Azure Key Vaultazure-key-vault, azureKeyVaultSecret-backed values in Azure Key Vault.
Azure Blob Storageazure-storage-blob, azureStorageBlobAzure container-backed object values.
Azure Table Storageazure-storage-table, azureStorageTableAzure table-backed key/value state.
Deno KVdeno-kv, denoKVDeno runtime KV storage.
Deno KV from Nodedeno-kv-node, denoKVNodeDeno KV through the Node-compatible client.
IndexedDBindexedbIndexedDB-backed values in a compatible browser runtime.
Web Storagelocalstorage, session-storage, sessionStorageBrowser local or session storage.
Capacitorcapacitor-preferences, capacitorPreferencesCapacitor 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:

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).
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 for application-owned ORM usage, integration schemas, PostgreSQL pools, Better Auth ownership, and migrations.

KV client or database client

ConfigUse it for
sqliteStorage(...)Farm KV storage with a Farm storage client.
redisStorage(...)Cache or queue-like key/value data.
storage.mountsMultiple named key/value stores.
storage.client with a Farm storage clientReuse a created Farm storage client as the root store.
storage.client with a DB/runtime objectGive 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.