Upstash Parity
AnvilBase gives you everything Upstash offers — durable message queues, a Redis-compatible KV cache, scheduled cron jobs, and a high-performance vector tier — without a second vendor, a second bill, or a second security model. It all runs in your own Postgres + Valkey, and an app reaches every surface with only a project key.
Each surface is a per-project, tenant-isolated data plane over HTTP, mirrored
into all of AnvilBase’s SDKs. The queue, cache, and cron data planes are
server-side surfaces: they require a project service_role key
(anon/authenticated keys get a 403), so use them from trusted backend code.
| Upstash product | AnvilBase equivalent | Backed by | SDK getter |
|---|---|---|---|
| Upstash QStash / Queues | Queues | PGMQ (Postgres) | client.queues |
| Upstash Redis | Cache / KV | Valkey (per-project ACL) | client.cache |
| Upstash QStash schedules | Cron & Scheduling | pg_cron (in your DB) | client.cron |
| Upstash Vector | Vector Search | pgvector + pgvectorscale | REST order=embedding.* |
Why this matters
- One backend, one key. No separate Upstash account, REST token, or region
to wire up. A project’s
service_rolekey unlocks queues, cache, and cron — the same key, the same client, the same isolation model as the rest of the platform. - Tenant isolation, enforced. Every surface is namespaced and confined per
project. Cache keys live under a dedicated Valkey ACL user
(
proj_<project_id>) that physically cannot read another project’s keyspace. Queues live in the project’s own database. Cron jobs are namespacedproj_<project_id>_<name>and pinned toplatform_<project_id>, so a job can only ever run a command inside its own project’s DB. - Transactional where it counts. Because queues are PGMQ inside your project database, enqueuing a message can share the same transaction as your data writes — no broker, no two-system consistency problem.
- Local = cloud. The exact same Docker Compose stack runs in development and production, so what you prove locally is what ships.
The three data planes
Queues (PGMQ)
Durable, transactional message queues inside Postgres. Send, read with a visibility timeout, ack, archive (the dead-letter-equivalent), replay, and read depth/age metrics.
import { createClient } from "@anvilbase/client";const client = createClient(url, serviceKey, { projectId });
await client.queues.create("jobs");const { data: sent } = await client.queues.queue("jobs").send({ task: "resize" });const { data: msgs } = await client.queues.queue("jobs").read(30); // 30s vtawait client.queues.queue("jobs").ack(msgs![0].msg_id);See the Queues guide for the full surface.
Cache / KV (Valkey)
A per-project, namespace-isolated KV cache. get/set (with ex/px/nx/xx),
incr/decr, expire/ttl/exists, mget/mset, and scan — bare keys,
with the <project_id>: namespace applied and enforced server-side.
await client.cache.set("session:42", token, { ex: 3600 });const hits = await client.cache.incr("ratelimit:1.2.3.4");const { data } = await client.cache.get("session:42");See the Cache guide for isolation details and the full surface.
Cron & Scheduling (pg_cron)
Schedule recurring SQL jobs that run inside your project’s own database — cleanups, rollups, queue draining. Schedule by a 5-field cron expression or a pg_cron interval, list jobs, read run history, and delete jobs.
await client.cron.scheduleJob("nightly", "0 3 * * *", "delete from sessions where expires_at < now()");const { data: jobs } = await client.cron.listJobs();const { data: runs } = await client.cron.jobRuns("nightly");See the Cron & Scheduling guide for the per-project scheduler model.
Migrating data in (from Upstash / Redis)
Moving an existing Upstash (or any Redis) dataset in is an operator-level
action: AnvilBase exposes no tenant-facing raw Redis endpoint. Each project’s
keys live under an enforced <project_id>: namespace (the hyphenated project
UUID, e.g. 123e4567-e89b-…:session:42), guarded by a per-project Valkey ACL
user. There is no inbound live sync — do this during a write-freeze window.
Cache / KV
Two options:
-
Through the API (recommended, namespace applied for you). Drain your source keys and re-write them with
client.cache.set/client.cache.mset— you pass bare keys and the<project_id>:prefix is added automatically:const client = createClient(url, serviceKey, { projectId });for (const { key, value, ttl } of exportFromUpstash()) {await client.cache.set(key, value, ttl ? { ex: ttl } : undefined);} -
Operator-level bulk copy with
redis-cli/RIOT, rewriting keys to include the<project_id>:prefix, against the bundled Valkey (host port 39637) authenticated with the platformVALKEY_PASSWORD(the default user — the per-project ACL user is namespace-locked and cannot be used for a bulk load):Terminal window # example: copy every key, prefixing the project namespace (HYPHENATED uuid)redis-cli -u "redis://:$UPSTASH_TOKEN@<upstash-host>:<port>" --scan | \while read -r k; dov=$(redis-cli -u "redis://:$UPSTASH_TOKEN@<upstash-host>:<port>" GET "$k")redis-cli -a "$VALKEY_PASSWORD" -p 39637 SET "<project_id>:$k" "$v"doneAfter a bulk copy, verify through the API:
client.cache.get(<bareKey>).
Queues
There is no bulk PGMQ import; drain the source queue into
client.queues.queue(<name>).send (create the queue first):
await client.queues.create("jobs");for (const msg of drainUpstashQueue()) { await client.queues.queue("jobs").send(msg);}See the Cache guide and Queues guide for the full surfaces.
The vector performance tier
Upstash Vector’s equivalent lives in your database too: pgvector for
embeddings + similarity search, accelerated by pgvectorscale’s
DiskANN-backed index (StreamingDiskANN) for fast, high-recall ANN at scale.
Order REST results by cosine / L2 / inner-product distance
(order=embedding.cosine.…) — no separate vector store. See
Vector Search.
Prove it yourself
The examples/upstash-demo
example is a headless script that drives all three data planes end-to-end with
only a project key and asserts each step — the capstone proof of Upstash parity.
The bundled runner lives at scripts/upstash-demo.sh
in the repo root: it brings up an isolated stack, provisions a project, obtains a
service key, and runs that example through the full queue + cache + cron
lifecycle. Run it from the repository root:
./scripts/upstash-demo.shReference
- Data-plane API reference — the raw HTTP
surface behind
client.queues/.cache/.cron. - API keys & scopes — why these surfaces
require a
service_rolekey.