1. Introduction

What is NyxKV

NyxKV is a Redis-compatible key-value store built entirely in Nyx. The daemon compiles to a single ~290KB native binary with no external runtime dependencies, and supports 52 commands across strings, lists, sets, hashes, and Pub/Sub.

Architecture

When to use NyxKV

NyxKV is a good fit when you want a Redis-compatible store without running and maintaining your own Redis server. It works well for:

2. Getting Started

The public NyxKV service requires TLS and a token. Both come from the dashboard.

Prerequisites

Connect

$ export TOKEN=<paste your token here>
$ redis-cli -h nyxkv.com -p 6380 --tls -a $TOKEN PING
PONG

If you get Connection reset by peer, you forgot --tls. If you get NOAUTH, you forgot -a $TOKEN.

Your first commands

$ redis-cli -h nyxkv.com -p 6380 --tls -a $TOKEN
nyxkv.com:6380> SET hello "world"
OK
nyxkv.com:6380> GET hello
"world"

Understanding responses

NyxKV speaks RESP. Response types you will see:

3. Connect from the dashboard

The public NyxKV service is multi-tenant: there is no anonymous access. Every connection authenticates with a token issued by the dashboard.

1. Create an account

Go to dashboard.nyxkv.com/signup. Provide an email and a password. There is no credit card and no plan to choose — the free tier is the product.

2. Generate a token

Open dashboard.nyxkv.com/app/keys and click Create token. The token value is shown once; copy it into a password manager or an environment variable. You can create as many tokens as you need and revoke any of them from the same page.

3. Connect

Pass the token as the password to any Redis client, and enable TLS:

$ redis-cli -h nyxkv.com -p 6380 --tls -a $TOKEN
nyxkv.com:6380> SET hello "world"
OK

Common pitfalls

Local development: if you run the daemon yourself with ./nyxkv (no --public, no --tls), the docs in this book apply too — just drop --tls -a $TOKEN from the examples and the prompt becomes 127.0.0.1:6380>.

4. Strings

Strings are the simplest data type. Every key starts as a string unless you use list, set, or hash commands.

SET key value

Stores a string value. Overwrites any existing value and clears any TTL.

> SET user:1 "Alice"
OK

GET key

Retrieves the string value. Returns (nil) if the key doesn't exist.

> GET user:1
"Alice"
> GET nonexistent
(nil)

SETNX key value

Sets the value only if the key does not already exist. Returns 1 if set, 0 if skipped.

> SETNX lock:job42 "worker-1"
(integer) 1
> SETNX lock:job42 "worker-2"
(integer) 0
# worker-1 holds the lock

SETEX key seconds value

Sets a value with an expiration time (in seconds), atomically.

> SETEX session:abc 3600 "user:1"
OK
# Expires in 1 hour

GETSET key value

Sets a new value and returns the old value atomically.

> SET counter 10
OK
> GETSET counter 0
"10"
# Returned old value, counter is now 0

INCR key

Increments an integer value by 1. Creates the key with value 0 if it doesn't exist.

> SET visitors 0
OK
> INCR visitors
(integer) 1
> INCR visitors
(integer) 2

DECR key

Decrements an integer value by 1. Creates the key with value 0 if it doesn't exist.

> SET stock 50
OK
> DECR stock
(integer) 49

APPEND key value

Appends the value to the end of the string. Creates the key if it doesn't exist. Returns the new length.

> SET greeting "Hello"
OK
> APPEND greeting " World"
(integer) 11
> GET greeting
"Hello World"

STRLEN key

Returns the length of the string value in bytes. Returns 0 if the key doesn't exist.

> STRLEN greeting
(integer) 11

MSET key value [key value ...]

Sets multiple key-value pairs in a single command.

> MSET name "Alice" age "30" city "NYC"
OK

MGET key [key ...]

Gets multiple values in a single command. Returns (nil) for missing keys.

> MGET name age city missing
1) "Alice"
2) "30"
3) "NYC"
4) (nil)

Pattern: Atomic counter

# Page view counter — no race conditions
> INCR page:/docs/intro
(integer) 1
> INCR page:/docs/intro
(integer) 2
> GET page:/docs/intro
"2"

5. Lists

Lists are ordered sequences of strings. They support push/pop from both ends, making them ideal for queues and stacks.

LPUSH key value [value ...]

Pushes one or more values to the head (left) of the list. Creates the list if it doesn't exist.

> LPUSH tasks "Deploy v2"
(integer) 1
> LPUSH tasks "Fix bug #42"
(integer) 2

RPUSH key value [value ...]

Pushes one or more values to the tail (right) of the list.

> RPUSH logs "2024-01-01 started"
(integer) 1
> RPUSH logs "2024-01-01 completed"
(integer) 2

LPOP key

Removes and returns the first element (head).

> LPOP tasks
"Fix bug #42"

RPOP key

Removes and returns the last element (tail).

> RPOP tasks
"Deploy v2"

LLEN key

Returns the number of elements in the list.

> LPUSH items "a" "b" "c"
(integer) 3
> LLEN items
(integer) 3

LRANGE key start stop

Returns elements from index start to stop (inclusive). Supports negative indices: -1 is the last element.

> RPUSH colors "red" "green" "blue" "yellow"
(integer) 4
> LRANGE colors 0 -1
1) "red"
2) "green"
3) "blue"
4) "yellow"
> LRANGE colors 1 2
1) "green"
2) "blue"

LINDEX key index

Returns the element at the given index. Supports negative indices.

> LINDEX colors 0
"red"
> LINDEX colors -1
"yellow"

Pattern: Job queue

# Producer: push jobs to the left
> LPUSH jobs '{"type":"email","to":"alice@example.com"}'
(integer) 1
# Consumer: pop jobs from the right (FIFO)
> RPOP jobs
"{\"type\":\"email\",\"to\":\"alice@example.com\"}"

6. Sets

Sets are unordered collections of unique strings. Duplicates are automatically ignored.

SADD key member [member ...]

Adds one or more members. Returns the number of new members added (ignores duplicates).

> SADD tags "nyx" "fast" "tiny"
(integer) 3
> SADD tags "nyx"
(integer) 0
# "nyx" was already in the set

SREM key member [member ...]

Removes one or more members. Returns the number of members removed.

> SREM tags "tiny"
(integer) 1

SISMEMBER key member

Tests if a member exists in the set. Returns 1 if yes, 0 if no.

> SISMEMBER tags "nyx"
(integer) 1
> SISMEMBER tags "slow"
(integer) 0

SMEMBERS key

Returns all members of the set.

> SMEMBERS tags
1) "nyx"
2) "fast"

SCARD key

Returns the number of members (cardinality).

> SCARD tags
(integer) 2

Pattern: Unique visitors

# Track unique visitors per page
> SADD visitors:/home "user:1" "user:2" "user:1"
(integer) 2
> SCARD visitors:/home
(integer) 2
# user:1 counted only once

7. Hashes

Hashes store field-value pairs under a single key. Think of them as objects or dictionaries.

HSET key field value [field value ...]

Sets one or more field-value pairs. Returns the number of new fields added.

> HSET user:1 name "Alice" email "alice@example.com" age "30"
(integer) 3

HGET key field

Gets the value of a single field.

> HGET user:1 name
"Alice"
> HGET user:1 phone
(nil)

HDEL key field [field ...]

Deletes one or more fields. Returns the number deleted.

> HDEL user:1 age
(integer) 1

HGETALL key

Returns all fields and values as an interleaved array.

> HGETALL user:1
1) "name"
2) "Alice"
3) "email"
4) "alice@example.com"

HKEYS key

Returns all field names.

> HKEYS user:1
1) "name"
2) "email"

HVALS key

Returns all values.

> HVALS user:1
1) "Alice"
2) "alice@example.com"

HEXISTS key field

Tests if a field exists. Returns 1 or 0.

> HEXISTS user:1 email
(integer) 1

HLEN key

Returns the number of fields.

> HLEN user:1
(integer) 2

Pattern: User profile

# Store structured data as a hash
> HSET product:42 name "Widget" price "19.99" stock "150"
(integer) 3
# Read a single field
> HGET product:42 price
"19.99"
# Update one field without touching others
> HSET product:42 stock "149"
(integer) 0

8. Key Management

DEL key [key ...]

Deletes one or more keys of any type. Returns the number of keys deleted.

> SET temp "data"
OK
> DEL temp
(integer) 1
> DEL temp
(integer) 0

EXISTS key

Checks if a key exists (any type). Returns 1 or 0.

> EXISTS user:1
(integer) 1
> EXISTS nonexistent
(integer) 0

TYPE key

Returns the type of the value stored at key: string, list, set, hash, or none.

> SET name "Alice"
OK
> LPUSH items "a"
(integer) 1
> TYPE name
string
> TYPE items
list

KEYS pattern

Lists all keys matching the pattern. Use * for all keys.

> KEYS *
1) "user:1"
2) "name"
3) "items"
> KEYS user:*
1) "user:1"
Note: on the public service, KEYS only returns keys in your own namespace, capped at 100 results.

DBSIZE

Returns the number of keys in your namespace.

> DBSIZE
(integer) 3

9. Expiration & TTL

Any key can have an expiration time. Once expired, the key is automatically deleted on next access (lazy deletion).

EXPIRE key seconds

Sets a timeout on a key. After seconds elapse, the key is deleted. Returns 1 if set, 0 if key doesn't exist.

> SET session "active"
OK
> EXPIRE session 3600
(integer) 1
# Session expires in 1 hour

TTL key

Returns the remaining time to live in seconds.

> TTL session
(integer) 3598
> TTL user:1
(integer) -1
> TTL nonexistent
(integer) -2

PERSIST key

Removes the expiration from a key. Returns 1 if a TTL was removed, 0 otherwise.

> PERSIST session
(integer) 1
> TTL session
(integer) -1
Free tier: on the public service, every key has a hard 24-hour TTL. EXPIRE values larger than 86,400 seconds are clamped to 86,400. PERSIST cannot remove the cap.

Pattern: Session with auto-expiry

# Create a session that expires in 30 minutes
> SETEX sess:abc123 1800 '{"user":"alice","role":"admin"}'
OK
# Check remaining time
> TTL sess:abc123
(integer) 1798
# Extend on activity
> EXPIRE sess:abc123 1800
(integer) 1

10. Pub/Sub

Pub/Sub enables real-time messaging between clients. Publishers send messages to channels; all subscribers on that channel receive them instantly.

SUBSCRIBE channel

Subscribes to a channel. The connection enters subscriber mode and blocks, receiving messages as they arrive.

# Terminal 1: subscriber
> SUBSCRIBE notifications
1) "subscribe"
2) "notifications"
3) (integer) 1
# Waiting for messages...

PUBLISH channel message

Sends a message to all subscribers of the channel. Returns the number of subscribers who received it.

# Terminal 2: publisher
> PUBLISH notifications "New order #1234"
(integer) 1

The subscriber in Terminal 1 receives:

1) "message"
2) "notifications"
3) "New order #1234"

UNSUBSCRIBE channel

Unsubscribes from a channel and exits subscriber mode.

Limitation: in subscriber mode, only SUBSCRIBE, UNSUBSCRIBE, and PING are accepted. Pub/Sub is not available over TLS connections, so subscribers must use the local ./nyxkv daemon during development — the public TLS endpoint accepts all other commands.

Pattern: Real-time notifications

# Chat room
> SUBSCRIBE chat:general
# Another client:
> PUBLISH chat:general "alice: Hello everyone!"
# All subscribers in chat:general receive the message

11. Persistence

NyxKV writes data to disk as binary snapshots in a .ndb file with a CRC32 checksum.

How it works

SIGTERM behaviour: the SIGTERM handler that used to flush on shutdown is intentionally disabled in the current build — it ran inside an async-signal-unsafe context and corrupted snapshots. The bg-saver covers normal restarts; the worst case (kill -9) loses ≤1 s.

SAVE

Triggers a synchronous snapshot to dump.ndb. Blocks until complete.

> SAVE
OK

BGSAVE

Triggers a background snapshot. Returns immediately.

> BGSAVE
Background saving started

LASTSAVE

Returns the Unix timestamp of the last successful save.

> LASTSAVE
(integer) 1711987200
On the public service: SAVE and BGSAVE are admin commands and are not exposed to user connections. The bg-saver runs unconditionally for every account.

The .ndb format

Binary snapshot format with a header, entries, and a CRC32 checksum:

If the CRC32 doesn’t match on load, NyxKV reports corruption and refuses to start, instead of silently losing data.

12. Authentication & Multi-tenant

The public NyxKV service is multi-tenant. Each authenticated account has its own namespace, and tokens are issued and revoked from the dashboard, not the protocol.

Connecting with a token

AUTH token

Authenticates the connection. All subsequent commands operate in your account’s namespace. Most clients call this for you when you pass a password.

> AUTH a1b2c3d4e5f6...
OK
> SET mykey "mydata"
OK
# Internally stored as: alice::mykey

WHOAMI

Returns the current authentication status.

> WHOAMI
"alice:free"

How namespace isolation works

When authenticated, all keys are transparently prefixed with your account’s id. This means:

Tokens are managed in the dashboard

To create, list, or revoke tokens, sign in to dashboard.nyxkv.com/app/keys:

There is no TOKEN_CREATE/TOKEN_REVOKE/TOKEN_LIST command on the public service. Those exist on the daemon but are gated to admin connections only, so the dashboard is the single source of truth for who has access.

Pattern: per-environment tokens

Issue separate tokens for staging and production. If a token leaks, revoke just that one without affecting the other environment:

13. Client Libraries

NyxKV speaks RESP, so any Redis client library works. The public service requires TLS and a token — the snippets below show the minimum viable connect for popular languages.

Python (redis-py)

import os, redis

r = redis.Redis(
    host="nyxkv.com",
    port=6380,
    ssl=True,
    password=os.environ["NYXKV_TOKEN"],
)

r.set("key", "value")
val = r.get("key")         # b"value"
r.incr("counter")
r.lpush("queue", "job1")
r.hset("user:1", mapping={"name": "Alice", "age": "30"})

Node.js (ioredis)

import Redis from "ioredis";

const r = new Redis({
  host: "nyxkv.com",
  port: 6380,
  tls: {},                     // enable TLS
  password: process.env.NYXKV_TOKEN,
});

await r.set("key", "value");
const val = await r.get("key");  // "value"
await r.lpush("queue", "job1");
await r.hset("user:1", "name", "Alice");

Go (go-redis)

import (
    "crypto/tls"
    "os"
    "github.com/redis/go-redis/v9"
)

rdb := redis.NewClient(&redis.Options{
    Addr:      "nyxkv.com:6380",
    Password:  os.Getenv("NYXKV_TOKEN"),
    TLSConfig: &tls.Config{},
})

rdb.Set(ctx, "key", "value", 0)
val, _ := rdb.Get(ctx, "key").Result()
rdb.LPush(ctx, "queue", "job1")
rdb.HSet(ctx, "user:1", "name", "Alice")

Rust (redis-rs)

let token = std::env::var("NYXKV_TOKEN")?;
let url = format!("rediss://:{}@nyxkv.com:6380", token);

let client = redis::Client::open(url)?;
let mut con = client.get_connection()?;

redis::cmd("SET").arg("key").arg("value").execute(&mut con);
let val: String = redis::cmd("GET").arg("key").query(&mut con)?;
Local development: if you’re running ./nyxkv on your laptop without --public and --tls, drop the TLS config and the password from these snippets and point at localhost:6380.

14. Patterns & Recipes

Session store

Use SETEX to store sessions with automatic expiration.

# Create session (30 min TTL)
> SETEX sess:abc123 1800 '{"user":"alice","role":"admin"}'
OK
# Read session
> GET sess:abc123
"{\"user\":\"alice\",\"role\":\"admin\"}"
# Extend on activity
> EXPIRE sess:abc123 1800
(integer) 1
# Logout
> DEL sess:abc123
(integer) 1

Rate limiter

Use INCR with EXPIRE to implement a per-key rate limiter.

# Track requests per user per minute
> INCR rate:user:42
(integer) 1
> EXPIRE rate:user:42 60
(integer) 1
# On each request: INCR, check if > threshold, reject if so
> GET rate:user:42
"47"

Cache with TTL

Cache expensive computations with automatic invalidation.

# Cache API response for 5 minutes
> SETEX cache:api:/users 300 '[{"id":1,"name":"Alice"}]'
OK
# Check cache before calling API
> GET cache:api:/users
"[{\"id\":1,\"name\":\"Alice\"}]"
# Cache miss → call API → SETEX again

Job queue (FIFO)

Use LPUSH + RPOP for a first-in-first-out queue.

# Enqueue jobs
> LPUSH jobs '{"task":"send_email","to":"alice@example.com"}'
(integer) 1
> LPUSH jobs '{"task":"resize_image","id":42}'
(integer) 2
# Dequeue (oldest first)
> RPOP jobs
"{\"task\":\"send_email\",\"to\":\"alice@example.com\"}"
# Check queue length
> LLEN jobs
(integer) 1

Configuration store

Use hashes to store application configuration with per-field updates.

> HSET config:app max_retries "3" timeout "30" debug "false"
(integer) 3
# Update one setting without touching others
> HSET config:app debug "true"
(integer) 0
# Read all config
> HGETALL config:app
1) "max_retries"
2) "3"
3) "timeout"
4) "30"
5) "debug"
6) "true"

Leaderboard

Use INCR for simple score tracking.

> INCR score:alice
(integer) 1
> INCR score:alice
(integer) 2
> INCR score:bob
(integer) 1
> MGET score:alice score:bob
1) "2"
2) "1"

Feature flags

Use sets to manage feature flags per user group.

> SADD feature:dark-mode "user:1" "user:3" "user:7"
(integer) 3
# Check if user has the feature
> SISMEMBER feature:dark-mode "user:3"
(integer) 1
> SISMEMBER feature:dark-mode "user:5"
(integer) 0

15. Limits

There is one tier on NyxKV: free. There is no Pro plan. There is no Enterprise plan. The free tier is the product.

What you get

LimitValue
Requests per second1000
Maximum keys10,000
Maximum value size1 MB
TTL on every key24 hours
TLSrequired
Multi-tenant isolationincluded
Pub/Subincluded (over plain TCP, see chapter 10)

Free tier details

How rate limiting works

Per-account, sliding-window: every connection counts against your account’s budget. When the limit is exceeded, the server responds with:

-ERR rate limit exceeded

Pricing

$0. Forever. No credit card. No upgrade prompts. If you outgrow what the free tier offers, you have outgrown what this service is for — run the open-source daemon on your own host.

Server commands reference

PING [message]

Returns PONG, or echoes the message. Used for health checks and latency testing.

INFO

Returns server statistics in Redis INFO format: version, total commands processed, keyspace info.

CONFIG GET save

Stub for redis-benchmark compatibility. Disabled on the public service.

FLUSHDB / FLUSHALL

Admin-only on the daemon. Not available on the public service.

16. Agent Memory (beta)

What it is

Agent Memory is private, persistent memory for AI agents and the apps built around them — on top of plain NyxKV, not a separate service. It solves two things a coding agent does not give you on its own: memory that survives after a session ends, and memory that follows you across machines.

Under the hood it is just keys. Entries live at mem:<collection>:<key> inside your normal token namespace, isolated the same way every other key is (see chapter 12). There is no separate account, no separate token, no separate protocol — anything that speaks RESP can read and write agent memory.

Quickstart

Four steps: token, SDK, a round-trip, then the Claude Code plugin.

1. Token

Same token as everywhere else in NyxKV. Create one at dashboard.nyxkv.com/app/keys and save it where the SDK looks for it by default:

$ umask 077 && printf '%s' 'YOUR_TOKEN' > ~/.nyxkv-token

2. Build the SDK

nyx-kv-sdk is a small Nyx CLI that wraps the collection/key model below. Clone nyx-kv and build it with the Nyx toolchain:

$ cd sdk && nyx build
$ ./nyx-kv-sdk help

3. Put and get an entry

$ ./nyx-kv-sdk put notes readme "first entry"
OK
$ ./nyx-kv-sdk get notes readme
first entry

4. Install the Claude Code plugin

sdk/claude/ is a drop-in plugin with two core skills, memory-pull and memory-push (plus optional friction-share / friction-review for leaving notes between agent instances). Copy it wherever Claude Code loads your plugins from, then just ask in plain language: “pull my memory” at the start of a session, “save my memory” before you close it. Both call the SDK underneath.

5. Sync across machines

The plugin wraps exactly this — run it directly if you want to sync a directory of markdown files without an agent in the loop:

# machine A
$ ./nyx-kv-sdk sync-push myproject ./notes
sync-push: 3 archivo(s) de ./notes → colección myproject
# machine B, same token
$ ./nyx-kv-sdk sync-pull myproject ./notes
sync-pull: 3 archivo(s) de colección myproject → ./notes

The mem:* key model

Everything lives under the mem: prefix, which keeps it visually distinct from cache and session keys and lets the dashboard enumerate it without scanning your whole namespace:

A “collection” is just a namespace inside your token's keyspace — a project, a topic, whatever grouping is useful to you. The Claude Code plugin defaults to one collection per project (agentmem-<project directory name>), overridable with the NYXKV_MEMORY_COLLECTION env var.

Entry values are base64-encoded on the wire with a b64: prefix, so multiline markdown survives RESP intact. The SDK and the plugin encode/decode this transparently — if you inspect an entry directly with redis-cli GET, you will see the raw b64:... form, not plain text.

Viewing your memory

The dashboard has a built-in viewer at dashboard.nyxkv.com/app/memory. It lists every collection and key under your token's mem:* namespace, lets you open an entry to read the decoded content, and delete individual entries. It reuses your dashboard session — no separate memory-specific login.

Limits (beta)

Agent Memory is beta. The honest constraints, so there are no surprises:

NyxKV Book — nyxkv.com — built with Nyx