Release candidate — 1.0.0-rc.1
StitchAPI

Distributed stores

Attach a Redis, Cloudflare Workers KV, or Deno KV StitchStore so a stitch's throttle, sessions, and cache become fleet-wide — same call site, no backend in core. Includes the atomic-incr trade-off that decides which store fits.

A StitchStore is the one seam that turns the process-local pieces of a stitch — its rate-limit counters, its auth sessions and tokens, and its response cache — into fleet-wide state, with no change to the call site. Core ships no backend; you attach one of these drivers to a seam and every worker shares the same counters and sessions.

Each driver is a thin peer-dependency (or zero-dependency) package that imports no client of its own — it runs on a small structural surface, so the client you already use is a drop-in, and a test double is too. All three are proven against the same verifyStoreContract from stitchapi/testing.

Which store?

The deciding axis is atomic incr: a distributed throttle needs it (rate counters must increment exactly once under concurrency), while shared cache and sessions need only get / set.

PackageRuntimeAtomic incr → distributed throttleShared cache & sessionsTTL unit
@stitchapi/redisNode (TCP) and edge (Upstash HTTP)✅ Lua INCR + PEXPIREms
@stitchapi/deno-kvDeno / Deno Deploy · Node & Bun via @deno/kv✅ atomic compare-and-setms
@stitchapi/cloudflare-kvCloudflare Workers / Pages (Web-API only)❌ throws — back it with a Durable Objects (60s min)

Reach for Redis on Node — or when you want one store that also runs on the edge via Upstash; Deno KV on Deno Deploy, where the same handle is replicated globally; Cloudflare Workers KV when you're on Workers and need shared cache and sessions (and pair it with a Durable Object if you also need a throttle).

Redis — @stitchapi/redis

npm install @stitchapi/redis stitchapi

Attach a redisStore to a seam and its throttle budget and auth sessions become fleet-wide. The package imports no Redis client — wrap the one you already run with the matching adapter (ioredis and redis are optional peers; @upstash/redis is matched structurally, so it never weighs on Node-only users):

import { fromIoredis, redisStore } from '@stitchapi/redis';
import Redis from 'ioredis';
import { seam } from 'stitchapi';

const api = seam({
    store: redisStore(fromIoredis(new Redis(process.env.REDIS_URL))),
});

The same store runs on the edge with fromUpstash — Upstash speaks Redis over HTTP, so the identical atomic INCR + EXPIRE logic works from a Vercel or Cloudflare function with no TCP socket:

import { fromUpstash, redisStore } from '@stitchapi/redis';
import { Redis } from '@upstash/redis';

const store = redisStore(fromUpstash(Redis.fromEnv()));

incr must be atomic and set the key's TTL only when it creates the counter — the bundled adapters do this in a single Lua EVAL (INCR, then PEXPIRE only when the value is 1), so a rate window can't slide forever and a crash can't strand an immortal counter. With a shared store the rate limiter is even-spaced across the fleet — see distributed throttle for the exact pacing semantics. Namespace keys with keyPrefix to share one Redis with other data:

redisStore(fromIoredis(client), { keyPrefix: 'myapp:' });

Cloudflare Workers KV — @stitchapi/cloudflare-kv

npm install @stitchapi/cloudflare-kv stitchapi

Web-API only (no node:*), so it runs in a Worker, in Pages Functions, and in any other edge runtime. Bind a KV namespace and the read-heavy halves of a stitch — cache and shared sessions/tokens — become fleet-wide across every isolate:

import { cloudflareKvStore } from '@stitchapi/cloudflare-kv';
import { seam } from 'stitchapi';

export default {
    async fetch(req, env) {
        const api = seam({ store: cloudflareKvStore(env.MY_KV) });
        return handle(api, req);
    },
};

Workers KV has no atomic increment. It is last-write-wins get / put / delete only, so a throttle counter built on it would undercount under concurrency and silently break rate limiting. Rather than do that, cloudflareKvStore(...).incr(...) throws a documented error. For a distributed throttle, back it with a Durable Object — the single-writer, strongly-consistent counter an atomic incr requires. KV remains the right backend for cache and sessions, the common edge need.

KV's expirationTtl is in seconds with a 60-second minimum; the store reconciles that with the contract's millisecond TTLs (ttlMsmax(60, ceil(ttlMs / 1000)) s), so a value asked to live 5s lives 60s (harmless for caches and sessions). set(key, undefined) deletes; keyPrefix namespaces.

Deno KV — @stitchapi/deno-kv

npm install @stitchapi/deno-kv stitchapi

Zero runtime dependencies — it never touches the Deno global, running on a structural DenoKvLike surface that a real Deno.Kv satisfies as-is. On Deno Deploy the same handle is replicated globally, so a stitch's state is edge-native for free:

import { denoKvStore } from '@stitchapi/deno-kv';
import { seam } from 'stitchapi';

const api = seam({ store: denoKvStore(await Deno.openKv()) });

The same package runs on Node or Bun through the @deno/kv npm package:

import { openKv } from '@deno/kv';
import { denoKvStore } from '@stitchapi/deno-kv';
import { seam } from 'stitchapi';

const api = seam({ store: denoKvStore(await openKv(process.env.DENO_KV_URL)) });

Deno KV has no native INCR, so incr is an atomic compare-and-set loop (read value + versionstamp, then atomic().check(...).set(...).commit(), retrying if another isolate raced) — N concurrent increments net exactly +N. The TTL (expireIn, already in milliseconds) is set only on the increment that creates the counter, so a busy window never resets. keyPrefix namespaces every key for sharing one KV database.

See also

On this page