Per-server JSON store. Keys are namespaced to your plugin and the active server — you cannot read another plugin's keys, and your keys for server A are isolated from server B. Capability: storage:kv.
Get / set / delete
ctx.kv.set("welcome_channel", "1234567890")
ctx.kv.set("session", {"user": "alice"}, ttl_seconds=3600)
val = ctx.kv.get("welcome_channel")
ctx.kv.delete("session")
exists = ctx.kv.exists("welcome_channel")
Counters
ctx.kv.increment("messages_today") # +1
ctx.kv.increment("xp:user:42", amount=10)
ctx.kv.increment("stats", path="voice.hours") # dot-path into a JSON value
ctx.kv.decrement("lives_remaining")
increment(key, amount=1, *, path="") is atomic in a single round-trip — use it instead of get + modify + set. path is keyword-only and targets a nested field inside a JSON object value (decrement takes no path).
Bulk & listing
keys = ctx.kv.list(prefix="user:", limit=100) # [key, key, ...]
values = ctx.kv.list_values(prefix="user:", limit=100) # {key: value}
batch = ctx.kv.get_many(["a", "b", "c"]) # {key: value}, max 50
ctx.kv.set_many({"a": 1, "b": 2}) # max 25 pairs
total = ctx.kv.count(prefix="xp:")
list() returns key names only. When you need the stored values too, use list_values() — it avoids the N+1 of list + get.
Quotas. 50,000 keys per (server, plugin) plus a global cap of 500,000 keys per plugin across all servers, 64 KB max value, 512-byte key names. Exceeding a key-count quota raises KvQuotaError; an oversize key or value fails with a plain error. list() returns at most 1000 keys and list_values() at most 100 entries per call, so paginate with prefix ranges on big keyspaces.