Release candidate — 1.0.0-rc.9
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 capability table 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. Each one runs the same conformance.store kit from stitchapi/testing, which checks the capability groups a store implements: every store passes the base group, and Redis and Deno KV pass the counter, pacing and lease groups as well.

Which store?

The deciding axis is atomicity. Shared cache and sessions need only get / set, which every backend has. A fleet-wide throttle needs an atomic read-modify-write in the store — the pacing cell for rate, the lease pair for concurrency — and an eventually-consistent backend can't offer one.

PackageRuntimeFleet-wide throttleShared cache & sessionsTTL unit
@stitchapi/redisNode (TCP) and edge (Upstash HTTP)✅ Lua scripts, one round trip each✅ms
@stitchapi/deno-kvDeno / Deno Deploy · Node & Bun via @deno/kv✅ atomic compare-and-set✅ms
@stitchapi/cloudflare-kvCloudflare Workers / Pages (Web-API only)❌ per isolate — pair with a Durable Object✅s (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).

Capabilities

StitchStore requires only get and set. Every other verb is a capability: a feature uses it when the store has it, and degrades in a known way when it doesn't. Nothing is configured — the throttle and the cache look at which verbs the store has.

Storeget / setincrementreservelease / release
memoryStore() (core default, one process)✅✅✅✅
@stitchapi/redis (bundled adapters)✅✅✅✅
@stitchapi/deno-kv✅✅✅✅
@stitchapi/cloudflare-kv✅———
asyncStorageStore / expoSecureStore (on-device)✅✅——

A custom RedisDriver must implement increment; reserve and the lease pair are optional there too, and redisStore exposes only what the driver has.

What each capability buys, and what happens without it:

CapabilityUsed byWithout it
get / setcache, auth sessions and tokens, circuit, allrequired
reservethrottle.rate — one fleet-wide cursorrate falls back to increment: fleet-shared slots, but a fleet that starts mid-window can run at up to rate × workers until it catches up
incrementthrottle.rate, only when there's no reservewith no reserve either, rate paces per process
lease / releasethrottle.concurrency — fleet-wide slotsconcurrency caps per process

Bulk cache invalidation needs no capability: it writes a fresh generation with set, so it works on every store.

A limit that ends up per-process says so. On its first call the throttle emits one info event, topic throttle.per-process, naming the limit and the missing verbs — for example rate: the store has no reserve or increment. Route events to a trace sink to see it; it fires once per throttle, not per call. loggerSink logs info events at debug (and @stitchapi/pino and @stitchapi/nest leave them out), so to see this one at the usual log level raise its topic to warn.

Redis — @stitchapi/redis

npm install @stitchapi/redis@rc stitchapi@rc

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()));

increment 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@rc stitchapi@rc

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 is a get / set store. It is last-write-wins get / put / delete with no atomic read-modify-write, so cloudflareKvStore implements none of the optional capabilities — no increment, reserve or lease pair. Cache (bulk invalidation included) and sessions work fully. A throttle over it paces per isolate and announces that once with a throttle.per-process event. For a fleet-wide throttle at the edge, back it with a Durable Object — a single writer, strongly consistent — or run @stitchapi/redis over Upstash's HTTP API.

KV is also eventually consistent: a write can take about 60 seconds to reach every location. That includes a cache bulk invalidation (stitch.cache.invalidate(), seam.invalidate()), so for up to a minute a reader elsewhere may still serve the entries you just invalidated. Exact invalidation (stitch.invalidate(input)) is a set too, with the same lag.

Workers KV also allows one write per second to the same key. A bulk invalidation writes one generation key, so rapid back-to-back invalidate() calls (a loop, a webhook burst) can be rejected with a 429 — invalidate once per change, or debounce.

KV's expirationTtl is in seconds with a 60-second minimum; the store reconciles that with the contract's millisecond TTLs (ttl → max(60, ceil(ttl / 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@rc stitchapi@rc

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 increment 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