KV Store
A general-purpose JSON key-value store: hierarchical keys with TTL expiration, tagging, per-user privacy, full-text search, batch operations, atomic counters, RFC 7396 merge patching, and binary values.
When to reach for it: user preferences, feature flags, cached computations with TTL, view/like counters, tagged content metadata, scratch state for agent sessions.
When not to: distributed locks and compare-and-swap belong to Distributed State; schema-validated, environment-scoped configuration with inheritance belongs to Remote Config.
Concepts
- Key paths are hierarchical - slash-separated, like
config/db/host. A shared prefix makes related keys listable and countable together. - Values are JSON - any JSON value, including a bare (quoted) string or number. Raw bytes go through the separate binary endpoints instead.
- Tags are labels orthogonal to paths, for when one key belongs to several categories.
- Private keys are visible only to their creator: they vanish from other
users' lists and searches, and a direct read by someone else fails with
403 access_deniedrather than a silent miss. - Indexing is opt-in and not retroactive - only values written with
indexedparticipate in full-text search. To make an existing key searchable, re-set it. - Conditional writes -
nx(only if absent) andxx(only if present) avoid get-then-set races. They are mutually exclusive; combining them fails with400 nx and xx are mutually exclusive.
Store and read
snug kv set config/db/host '"localhost"' --ttl 3600
snug kv set user/prefs '{"theme":"dark"}' --tags ui,user --private
snug --output json kv get user/prefs
snug kv get user/prefs --json-path '$.theme' # partial read
snug kv head config/db/host # cheap existence check, exits non-zero if missing
snug kv delete config/db/host --forceOver HTTP the same operations are PUT/GET/HEAD/DELETE on
/api/v1/kv/{path}:
curl -X PUT -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": {"greeting": "hello world"}}' \
http://localhost:4000/api/v1/kv/docs/hello{
"status": 201,
"msg": "Created",
"data": {
"key": "docs/hello",
"owner": "docs-writer",
"private": false,
"content_type": "application/json",
"size_bytes": 26,
"tags": [],
"indexed": false,
"created_at": "2026-08-28T03:41:02.485104Z",
"updated_at": "2026-08-28T03:41:02.485104Z"
}
}The value must be valid JSON - the CLI parses it locally before sending, so
a bare hello is rejected client-side; write '"hello"'.
Counters
incr atomically adjusts a numeric value. It does not create missing
keys - unlike Redis INCR, a missing key is 404 key_not_found, and a
non-numeric value is 400 type_mismatch. Initialize first:
snug kv set stats/page-views '0' --nx
snug kv incr stats/page-views
snug kv incr stats/errors --delta -1 # decrementPartial updates
patch applies an RFC 7396 merge patch - a deep merge, so sibling
fields survive. Verified:
snug kv set docs/settings '{"nested":{"x":1,"y":2}}'
snug kv patch docs/settings '{"nested":{"z":3}}'
snug --output json kv get docs/settings{ "value": { "nested": { "x": 1, "y": 2, "z": 3 } } }To delete a field, set it to JSON null in the patch. To replace a nested
object outright, use set.
TTL
snug kv set cache/session/abc '{"user":"alice"}' --ttl 1800
snug kv ttl cache/session/abc --sliding # auto-extend on every read
snug kv ttl cache/session/abc --extend 3600 # reset to N seconds from now
snug kv ttl cache/session/abc --remove # make permanentBatch operations
snug kv batch-get config/a,config/b,config/c
snug kv batch-set --file items.json
snug kv batch-delete config/old/a,config/old/b --forcebatch-set --file takes a bare JSON array; each item supports key,
value, ttl_seconds (not ttl), tags, indexed, private:
[
{ "key": "config/a", "value": "hello", "ttl_seconds": 3600 },
{ "key": "config/b", "value": {"nested": true}, "tags": ["ui"], "indexed": true }
]Batch size is capped (100 keys by default; see the configuration reference).
Binary values
Raw bytes - images, PDFs - go through dedicated endpoints that stream
application/octet-stream:
snug kv set-binary images/logo.png --file ./logo.png --ttl 86400 --private
snug kv get-binary images/logo.png --out ./logo-copy.pngA binary key lists and deletes like any other. incr and patch reject it
with 400 type_mismatch, and a JSON get of a binary key is
404 key_not_found - use get-binary. For large files with signed
download URLs, use the Blob service instead.
Finding keys
Listing, tag and owner filters, full-text search over indexed values, and aggregate stats have their own page: Search and discovery.
Limits and configuration
Values up to 4 MB, key paths up to 512 characters, and 20 tags per key by
default - all tunable, along with default TTL and the expiration worker,
via the KV_* variables in the
KV CONFIG reference.
Reference
- KV Store API - every endpoint, callable
- Related: Distributed State for locks and CAS, Remote Config for environment-scoped config, Blob for large files with signed URLs