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.
| Package | Runtime | Atomic incr → distributed throttle | Shared cache & sessions | TTL unit |
|---|---|---|---|---|
@stitchapi/redis | Node (TCP) and edge (Upstash HTTP) | ✅ Lua INCR + PEXPIRE | ✅ | ms |
@stitchapi/deno-kv | Deno / Deno Deploy · Node & Bun via @deno/kv | ✅ atomic compare-and-set | ✅ | ms |
@stitchapi/cloudflare-kv | Cloudflare Workers / Pages (Web-API only) | ❌ throws — back it 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).
Redis — @stitchapi/redis
npm install @stitchapi/redis stitchapiAttach 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 stitchapiWeb-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 (ttlMs →
max(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 stitchapiZero 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
Sentry
A Sentry TraceSink that maps the stitch event stream to breadcrumbs and captures error events with the call's context — metadata-only, safe on a secret-bearing seam.
AWS SigV4
awsSigV4 signs each request with AWS Signature Version 4 — the request-signing AuthStrategy that core's built-in bearer/apiKey/basic/oauth2 don't cover. Edge-safe Web Crypto, no dependencies.