CapyDB/ docs
GuidesK/V

K/V

CapyDB Knight/Valkyrie - a managed key-value store and rate limiter in its own isolated cell, compatible with the Upstash REST protocol and ordinary Redis® OSS clients.

A K/V store is a managed key-value service, one per project, running in its own KV cell - the same isolated runtime primitive a database cell is. It is powered by Valkey and speaks two protocols: the Upstash REST protocol over HTTPS, and RESP on the wire.

It is a feature in its own right, not an add-on to the database. A project can have a K/V store and never use its database, which is a complete way to use CapyDB - plenty of applications want a rate limiter and nothing else.

What it is for

Work that wants a fast counter rather than a table:

UseWhy K/V and not Postgres
Rate limitingA limit check runs on every request; a row write per request is the expensive way to count
SessionsShort-lived, high-churn, and nobody queries them relationally
Queues and locksBLPOP and SET NX PX are one round trip
Feature flagsRead on every request, written rarely
CachingBounded memory with an eviction policy, which a table does not have

Anything you would be unhappy to lose belongs in the database cell instead. See Durability below.

Create a store

From the dashboard, open a project and go to K/V → Add K/V store. It provisions in seconds.

Over the API:

POST /v1/projects/{id}/kv
curl -X POST https://capydb.dev/api/capydb/v1/projects/$PROJECT_ID/kv \
  -H "Authorization: Bearer $CAPYDB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

The response carries the store, the provisioning job, and the credentials:

{
  "job": { "id": "job_...", "type": "kv.create", "state": "pending" },
  "kv_store": {
    "id": "kv_...",
    "state": "provisioning",
    "maxmemory_mb": 128,
    "maxmemory_policy": "volatile-lru",
    "persistence": "rdb",
    "credentials": {
      "rest_url": "https://<project>.db.capydb.dev",
      "rest_token": "capy_kv_...",
      "redis_url": "rediss://default:capy_kv_...@<project>.db.capydb.dev:6379"
    }
  }
}

The plaintext token is returned on create and rotate only. Only its SHA-256 hash is stored, so it cannot be read back afterwards - GET .../kv/credentials returns the endpoints with token_required: true and no secret. If you lose the token, rotate to mint a new one.

Connect

Two environment variables, and any Upstash-compatible client finds the store:

.env
CAPYDB_KV_REST_URL="https://<project>.db.capydb.dev"
CAPYDB_KV_REST_TOKEN="capy_kv_..."
import { Redis } from '@upstash/redis'

const redis = new Redis({
  url: process.env.CAPYDB_KV_REST_URL!,
  token: process.env.CAPYDB_KV_REST_TOKEN!,
})

await redis.set('hello', 'world')

Redis.fromEnv() reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN, which CapyDB does not publish. Construct the client explicitly, as above, or set those names yourself.

There is no CapyDB SDK to install for K/V. If you would rather not hand-wire the client, @capydb/kv is a small factory that reads the CapyDB variable names and returns an @upstash/redis client - see Clients.

capydb kv create --write-env puts both variables in your local env file, and the Vercel, Netlify and Cloudflare integrations push CAPYDB_KV_REST_URL to your deployment on their own. They cannot push the token - nobody can read it back - so that one is yours to set wherever your app runs.

Capacity

K/V is included in every plan at that plan's size. There is no separate K/V subscription and no per-command charge.

PlanStore capacity
Vibe32 MB
Ship128 MB
Business512 MB

The figure is the storable maxmemory. The KV cell's own memory ceiling is twice that, so a snapshot fork has headroom - the extra is not usable capacity.

When a store reaches capacity, the eviction policy decides what happens. It is volatile-lru: the least recently used key that has a TTL is discarded to make room.

This matters. A key with no expiry is never evicted, so a store filled with non-expiring keys has nothing it is allowed to discard and starts rejecting writes with an out-of-memory error. Set a TTL on anything you would be happy to lose - which, in a store with this durability posture, is everything.

Durability

A K/V store keeps a periodic snapshot. It survives a restart of the cell. It is not a database:

  • No backups. A K/V store is not included in your project's backups.
  • No point-in-time recovery. PITR covers the database cell only.
  • No replica.
  • Eviction. Once full, keys are discarded by policy.

Treat a store as fast, expendable state. This is the same posture every managed Redis-compatible service takes, and it is stated plainly here rather than buried, because the failure mode - treating a cache as a system of record - is expensive and quiet.

Isolation

A KV cell gets what a database cell gets: its own process, its own memory ceiling and CPU share enforced by the kernel, its own storage, its own credential and its own socket. There is no shared instance and no shared keyspace.

The store has no network route to the public internet and none to your database. The only way in is the authenticated endpoint. See Security.

Placement

A store is placed in the same region as its project, which is the point: a rate-limit check that crosses an ocean costs more than the request it is protecting. K/V is available where the region has K/V capacity; a region that has none yet returns a clear error at create time rather than provisioning something slow.