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:
| Use | Why K/V and not Postgres |
|---|---|
| Rate limiting | A limit check runs on every request; a row write per request is the expensive way to count |
| Sessions | Short-lived, high-churn, and nobody queries them relationally |
| Queues and locks | BLPOP and SET NX PX are one round trip |
| Feature flags | Read on every request, written rarely |
| Caching | Bounded 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:
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:
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.
| Plan | Store capacity |
|---|---|
| Vibe | 32 MB |
| Ship | 128 MB |
| Business | 512 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.