ESC
Type to search...
S
Soli Docs

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

See also