KV
Full-featured key-value store backed by SoliKV. Supports strings, counters, lists, sets, hashes, sorted sets, bitmaps, and HyperLogLog with Redis-compatible commands. Shares the same connection as Cache but operates on raw keys with no prefix.
Configuration
KV shares the SoliKV connection with Cache. Configure via environment variables or programmatically:
| Variable | Description | Default |
|---|---|---|
SOLIKV_RESP_HOST |
SoliKV host | localhost |
SOLIKV_RESP_PORT |
RESP port | 6380 |
SOLIKV_TOKEN |
Bearer token (optional) | — |
SOLIKV_RESP_HOST=localhost
SOLIKV_RESP_PORT=6380
SOLIKV_TOKEN=my-secret-token
KV.configure("my-solikv-host", "my-secret-token")
Read & Write
KV.set(key, value, ttl?)
Store a value. Optionally set TTL in seconds. Without TTL the key lives forever.
KV.set("user:1", { "name": "Alice" })
KV.set("temp:token", "abc123", 60) # expires in 60 seconds
KV.get(key)
Retrieve a value. Returns null if the key doesn't exist.
user = KV.get("user:1") # => { "name": "Alice" }
missing = KV.get("nope") # => null
KV.delete(key)
Delete a key. Returns true if the key was removed, false if it didn't exist.
KV.exists(key)
Check if a key exists. Returns Bool.
if KV.exists("user:1")
println("User exists")
end
KV.keys(pattern?)
denied by default
List keys matching a glob pattern. Defaults to "*".
Gated: Bulk key enumeration is O(N) and exposes the entire keyspace. Set SOLI_KV_ALLOW_ADMIN=1 to enable.
all = KV.keys # all keys
users = KV.keys("user:*") # keys starting with "user:"
KV.type(key)
Get the data type of a key — string, list, set, hash, or none.
KV.rename(key, newkey)
Rename a key. Errors if newkey already exists.
Strings
KV.setnx(key, value)
Set a key only if it does not already exist. Returns true if it was set. Handy for locks and "first writer wins".
if KV.setnx("lock:job", "held")
run_job()
end
KV.getset(key, value) / KV.getdel(key)
Atomically swap in a new value and return the old one, or read-and-delete in one step. Both return the previous value (or nil).
KV.append(key, value) / KV.strlen(key)
Append to a string value (returns the new length) or read the current length.
KV.mget(...keys) / KV.mset(key, value, ...)
Read or write many keys in a single round-trip. mget returns an array (nil for missing keys); mset takes alternating key/value pairs.
KV.mset("a", "1", "b", "2")
KV.mget("a", "b", "missing") # => ["1", "2", nil]
TTL Management
KV.ttl(key)
Remaining TTL in seconds. Returns null if the key has no expiry or doesn't exist.
KV.expire(key, seconds)
Set a TTL on an existing key. Returns true if successful.
KV.expire("user:1", 3600) # key expires in 1 hour
KV.persist(key)
Remove the TTL and make the key persistent. Returns true on success.
KV.pexpire(key, milliseconds) / KV.pttl(key)
Millisecond-precision versions of expire / ttl. pttl returns the remaining milliseconds, or nil.
KV.expireat(key, unix_timestamp)
Expire a key at an absolute Unix time (seconds). Returns Bool.
KV.touch(...keys) / KV.unlink(...keys)
touch bumps last-access time and returns how many keys existed; unlink deletes keys without blocking and returns the count removed.
Counters
KV.incr(key) / KV.decr(key)
Atomically increment or decrement by 1. Creates the key with value 0 if it doesn't exist. Returns the new value.
KV.incr("visits") # => 1
KV.incr("visits") # => 2
KV.decr("visits") # => 1
KV.incrby(key, amount) / KV.decrby(key, amount)
Increment or decrement by a specified amount. Returns the new value.
KV.incrby("score", 10) # => 10
KV.decrby("score", 3) # => 7
KV.incrbyfloat(key, amount)
Increment by a floating-point amount. Returns the new value as a Float.
Lists
KV.lpush(key, ...values) / KV.rpush(key, ...values)
Push values to the head (left) or tail (right) of a list. Returns the new list length.
KV.rpush("queue", "job1")
KV.rpush("queue", "job2")
KV.lpush("queue", "urgent")
KV.lpop(key) / KV.rpop(key)
Remove and return the first (left) or last (right) element.
KV.lrange(key, start, stop)
Get a range of elements. Use 0, -1 for the entire list.
all = KV.lrange("queue", 0, -1)
KV.llen(key)
Get the length of a list.
KV.lindex(key, index) / KV.lset(key, index, value)
Read or overwrite the element at index (negative counts from the tail). lindex returns the element or nil.
KV.lrem(key, count, value) / KV.ltrim(key, start, stop)
lrem removes count occurrences of a value (returns how many were removed); ltrim keeps only the elements in the given range.
KV.rpoplpush(source, dest)
Atomically pop from the tail of source and push to the head of dest — the building block for reliable work queues. Returns the moved element or nil.
job = KV.rpoplpush("queue", "processing")
Sets
KV.sadd(key, ...members) / KV.srem(key, ...members)
Add or remove members from a set. Returns the number of elements actually added or removed.
KV.sadd("tags", "rust", "soli", "redis")
KV.srem("tags", "redis")
KV.smembers(key)
Get all members of a set. Returns an array.
KV.sismember(key, member)
Check if a value is a member of a set. Returns Bool.
if KV.sismember("beta-users", user_id)
render("beta/feature")
end
KV.scard(key)
Get the number of members in a set.
KV.smismember(key, ...members)
Check membership for several values at once. Returns an array of Bool, one per member.
KV.spop(key, count?) / KV.srandmember(key, count?)
Return random members — spop removes them, srandmember leaves them in place. Pass count to get an array.
KV.sinter(...keys) / KV.sunion(...keys) / KV.sdiff(...keys)
Set intersection, union, and difference across two or more sets. Each returns an array.
KV.sadd("a", "x", "y", "z")
KV.sadd("b", "y", "z", "w")
KV.sinter("a", "b") # => ["y", "z"]
KV.sdiff("a", "b") # => ["x"]
KV.smove(source, dest, member)
Atomically move a member from one set to another. Returns Bool.
Hashes
KV.hset(key, field, value)
Set a field in a hash. Creates the hash if it doesn't exist.
KV.hset("user:1", "name", "Alice")
KV.hset("user:1", "email", "alice@example.com")
KV.hget(key, field)
Get a single field value from a hash. Returns null if the field or key doesn't exist.
KV.hgetall(key)
Get all fields and values as a Soli Hash.
user = KV.hgetall("user:1")
println(user["name"]) # => "Alice"
KV.hdel(key, ...fields)
Delete one or more fields from a hash. Returns the number of fields removed.
KV.hexists(key, field)
Check if a field exists in a hash. Returns Bool.
KV.hkeys(key) / KV.hlen(key)
Get all field names in a hash or the number of fields. hkeys returns an array, hlen returns an int.
KV.hsetnx(key, field, value)
Set a field only if it doesn't already exist. Returns Bool.
KV.hmget(key, ...fields) / KV.hvals(key)
hmget reads several fields at once (nil for missing); hvals returns all values. Both return an array.
KV.hincrby(key, field, amount) / KV.hincrbyfloat(key, field, amount)
Increment a hash field by an integer or floating-point amount. Returns the new value (Int or Float).
Sorted Sets
Sorted sets keep members ordered by an associated floating-point score — the natural fit for leaderboards, priority queues, and time-ordered feeds.
KV.zadd(key, score, member, ...) / KV.zrem(key, ...members)
Add score/member pairs (returns the number of new members) or remove members.
KV.zadd("scores", 100, "alice", 80, "bob", 120, "carol")
KV.zscore(key, member) / KV.zincrby(key, amount, member)
Read a member's score (Float, or nil) or bump it by amount (returns the new score).
KV.zrank(key, member) / KV.zrevrank(key, member)
0-based rank ascending or descending by score. Returns the rank or nil if the member is absent.
KV.zcard(key) / KV.zcount(key, min, max)
Total number of members, or the count whose score falls within [min, max].
KV.zrange(key, start, stop, with_scores?) / KV.zrevrange(...) / KV.zrangebyscore(key, min, max)
Fetch members by rank (ascending or descending) or by score range. Pass true for with_scores to interleave scores in the result.
# Top 3 of a leaderboard, with scores
KV.zrevrange("scores", 0, 2, true)
Bitmaps
KV.setbit(key, offset, value) / KV.getbit(key, offset)
Set or read a single bit at offset. setbit returns the previous bit; getbit returns 0 or 1. Great for compact per-user flags.
KV.bitcount(key)
Count the set bits in a value — e.g. how many days a user was active in a daily-activity bitmap.
HyperLogLog
A HyperLogLog estimates the number of distinct items in a stream (its cardinality) using a fixed ~12 KB sketch — whether you add a hundred items or a hundred million. It trades a small, bounded error (~0.81% standard error) for memory that stays constant. Reach for it to count unique visitors, IP addresses, or events at scale, where an exact set would grow without bound.
KV.pfadd(key, ...elements)
Add one or more elements to the HyperLogLog at key (created on first use). Returns 1 if the estimate likely changed, else 0.
KV.pfadd("visitors:2026-06-24", "alice", "bob", "alice")
KV.pfadd("visitors:2026-06-24", "carol")
KV.pfcount("visitors:2026-06-24") # => ~3 (an estimate, not exact)
KV.pfcount(key, ...keys)
Estimated cardinality. Pass several keys to get the cardinality of their union without modifying any of them.
# Union estimate across two days, computed on the fly
KV.pfcount("visitors:2026-06-23", "visitors:2026-06-24")
KV.pfmerge(destkey, ...sourcekeys)
Merge the source HyperLogLogs into destkey (the union). Returns nil.
KV.pfmerge("visitors:week", "visitors:2026-06-23", "visitors:2026-06-24")
KV.pfcount("visitors:week") # => ~unique across both days
The count is a probabilistic estimate (~0.81% standard error), not an exact figure — use it when "roughly how many uniques" is good enough and memory matters.
Server Commands
KV.ping
Check connectivity with the SoliKV server. Returns "PONG".
KV.dbsize
Total number of keys in the SoliKV database.
KV.flushdb
denied by default
Delete all keys. Gated behind SOLI_KV_ALLOW_ADMIN=1.
KV.cmd(...args)
Run a raw SoliKV command. The first argument is the command verb, filtered through the admin denylist.
KV.cmd("SET", "key", "value")
KV.cmd("GET", "key")
KV.cmd("EXPIRE", "key", 60)
Admin denylist
KV.cmd, KV.flushdb, and KV.keys refuse destructive or keyspace-wide operations by default. The command verb is matched (case-insensitive) against:
FLUSHALL FLUSHDB KEYS SCAN
CONFIG DEBUG SHUTDOWN MONITOR CLIENT
SLAVEOF REPLICAOF BGREWRITEAOF BGSAVE SAVE
CLUSTER FAILOVER RESET ACL
SCRIPT EVAL EVALSHA FUNCTION
To enable raw admin access, set SOLI_KV_ALLOW_ADMIN=1 on the process that needs it. Leave this unset on workers reachable from user traffic — a controller bug or template injection cannot reach KV.cmd("FLUSHALL").
Common Patterns
Rate limiting with counters
def check_rate_limit(ip)
key = "rate:" + ip
count = KV.incr(key)
KV.expire(key, 60) if count == 1
return count <= 100
end
Job queue with lists
KV.rpush("jobs", JSON.stringify({"type": "email", "to": "alice@example.com"}))
raw = KV.lpop("jobs")
if raw != null
job = JSON.parse(raw)
process_job(job)
end
User sessions with hashes
KV.hset("session:abc", "user_id", str(user.id))
KV.hset("session:abc", "role", user.role)
KV.expire("session:abc", 3600)
# Later
session = KV.hgetall("session:abc")
println("User ID: " + session["user_id"])
Feature flags with sets
KV.sadd("features:beta", "user:1", "user:2")
if KV.sismember("features:beta", "user:" + str(user.id))
render("beta/feature")
else
render("stable/feature")
end