All docs

Docs · Reference

KV store

persist values across requests

txco://kv/ is the one place an op can both read AND write durable state. The envelope lives for a single request; the KV store outlives it, so rules can keep counters, flags, locks, cached lookups — any small JSON — between requests._

Pick a backend with --kvstore:

  • boltdb (default) — an embedded on-disk store, local to that one chassis. Zero setup; right for a single chassis or dev.
  • redis — a shared redis that several chassis point at (--kvstore-addrs), so they all read and write the same keys. Use it when more than one chassis serves the same tenants and they need to see each other’s writes.

The ops are identical either way.

The ops

OpWITHDoes
txco://kv/getkey, into?, fallback?Read a value into the envelope.
txco://kv/setkey, value? / from?, ttl?Write a value.
txco://kv/deletekeyRemove a value.
txco://kv/incrkey, by?, ttl?, into?Atomically add to an integer.
txco://kv/caskey, value? / from?, expected?, ttl?, into?Check-and-set.

Every op also takes an optional namespace (see below).

Keys are scoped per tenant + namespace

Each key is stored under <tenant>/<namespace>/<key>:

  • tenant — the request’s resolved tenant. You can’t reach another tenant’s keys.
  • namespace — defaults to the stack serving the request, so one stack’s keys never collide with another’s. Pass namespace = "shared" (any name) to share keys across a tenant’s stacks.
  • key — yours; no . or / (use a namespace to group).

So kv/incr key="hits" from stack web of tenant acme touches acme/web/hits.

Values are JSON; results land at into

Values are arbitrary JSON. kv/get / kv/incr / kv/cas write their result into the envelope at into (default _kv). _kv is _-prefixed, so it’s dropped from the default web response — a scratch slot the client never sees. Point into at a non-private path to surface a value, e.g. into = ".count".

# read a counter into the response, defaulting to 0 when unset
WITH key = "hits", into = ".hits", fallback = 0
EXEC "txco://kv/get"

The fallback param is fallback, not defaultdefault is a reserved txcl keyword.

set / delete

# a literal value
WITH key = "greeting", value = "hello"
EXEC "txco://kv/set"

# a value pulled from an envelope path
WITH key = "ua", from = "@web.req.headers.user-agent.0"
EXEC "txco://kv/set"

WITH key = "greeting"
EXEC "txco://kv/delete"

TTL — values can expire (opt-in)

kv/set and kv/incr take an optional ttl in seconds. Omit it (the default) and the key is persistent — it lives until you overwrite or delete it. With a ttl, the key vanishes once it lapses.

# a 10-minute cache entry
WITH key = "rates", from = ".fetched", ttl = 600
EXEC "txco://kv/set"

An operator can cap the maximum with --kv-max-ttl (a larger requested ttl clamps down to it).

Atomic counters — kv/incr

kv/incr adds by (default 1) to an integer key and writes the new value to into. It’s atomic — concurrent requests never lose an update, even across chassis sharing one redis. by is signed, so a negative by decrements.

WITH key = "page:hits", by = 1, into = ".count"
EXEC "txco://kv/incr"

WITH key = "inventory", by = -1, into = ".left"
EXEC "txco://kv/incr"

Check-and-set — kv/cas

kv/cas writes a new value only if the current value equals expected — or, with expected omitted, only if the key is absent. It reports {swapped, current} at into: swapped is whether it wrote, and current is the value now in the store. On a failed check current is the real current, so you can recompute and retry.

# optimistic update — write only if nobody changed it since you read it
WITH key = "config", expected = .prev, value = .next
EXEC "txco://kv/cas"
#   ._kv.swapped == false → ._kv.current holds the latest; retry against it

With expected omitted it’s a lock — only the first caller wins:

# scope 100 — try to take the lock
WITH key = "job:42:lock", value = "me"
EXEC "txco://kv/cas"

# scope 200 — proceed only if we got it (later scope: same-scope ops run in parallel)
WHEN ._kv.swapped == true
EMIT .status = "running"

That’s also how you build a safe state machine: kv/get to read, decide in a rule, then kv/cas with the value you read as expected — the write only lands if the world hasn’t moved under you.

Notes

  • KV ops pay normal fuel and appear in traces.
  • Values over --kv-max-value-bytes (default 64 KiB) are rejected.
  • With boltdb each chassis keeps its own store; switch to redis when several chassis must share state.

Edit this page · View as markdown