Skip to content

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 productAnvilBase equivalentBacked bySDK getter
Upstash QStash / QueuesQueuesPGMQ (Postgres)client.queues
Upstash RedisCache / KVValkey (per-project ACL)client.cache
Upstash QStash schedulesCron & Schedulingpg_cron (in your DB)client.cron
Upstash VectorVector Searchpgvector + pgvectorscaleREST order=embedding.*

Why this matters

  • One backend, one key. No separate Upstash account, REST token, or region to wire up. A project’s service_role key 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 namespaced proj_<project_id>_<name> and pinned to platform_<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 vt
await 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 platform VALKEY_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; do
    v=$(redis-cli -u "redis://:$UPSTASH_TOKEN@<upstash-host>:<port>" GET "$k")
    redis-cli -a "$VALKEY_PASSWORD" -p 39637 SET "<project_id>:$k" "$v"
    done

    After 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:

Terminal window
./scripts/upstash-demo.sh

Reference