Storage¶
Where a game or a server keeps what outlives a session: player progress,
inventories, world saves, counters, scores. One small interface
(kke/storage/Store.h), and the place behind it is a setting:
| URL | What it is | When |
|---|---|---|
sqlite:save/world.db (or just the path) |
Built in. One file, SQLite (public domain). Nothing to install or run | The default. A game on a PC, a server for friends, most community servers |
memory: |
Nothing on disk | Tests, a match nobody saves |
valkey://host:6379 |
A Valkey server (BSD; the free fork of Redis, which speaks the same protocol, so redis:// works too) |
Many game servers sharing fast state: who is online where, match queues, counters |
postgres://user:pw@host/db |
A PostgreSQL database (PostgreSQL licence, free) | A studio or community with one big shared database and its own backup tools |
Everything is free to use and to ship. Valkey support is built by default
(KKE_ENABLE_VALKEY, client: hiredis, BSD); PostgreSQL needs the system's
libpq (apt install libpq-dev, then -DKKE_ENABLE_POSTGRES=ON; the
server Docker image has it).
Why not Redis or Postgres by default: both are servers someone has to run, and "a 5-year-old can make a game" means a game that saves with nothing to set up. SQLite is the most deployed database there is, and fast: a save is one file you can copy. When a game grows into many servers, the same code points at Valkey or Postgres with one setting. (Redis itself is no longer open source since 2024; Valkey is the Linux Foundation fork that stayed BSD.)
Using it¶
#include "kke/storage/Store.h"
std::string error;
auto store = kke::storage::openStore("sqlite:save/world.db", &error);
if (!store) log->error("can't open the save: {}", error);
store->put("players", "kees", R"({"level": 3, "gold": 120})");
auto kees = store->get("players", "kees"); // std::optional<std::string>
store->increment("stats", "goblins.killed", 1); // counters (atomic on every backend)
for (auto& item : store->list("players", "k")) // keys starting with "k", in order
...;
// All or nothing: a trade that fails halfway changes nothing.
store->transaction([&] {
return store->put("inventory.kees", "sword", "1") && store->put("inventory.ann", "sword", "0");
});
- A collection names a kind of thing (
players,inventory.kees,world): 1-64 ofa-z 0-9 _ - .. A key is 1-256 bytes, a value up to 16 MiB of anything (JSON, a packed save). - Nothing throws: a call that fails returns false / nothing and
lastError()says why (a full disk, a server that went away). Stores are safe to use from several threads. - Transactions nest (an inner one called off undoes only its part). On
Valkey a transaction's writes land together or not at all, but it does
not lock what it read against other servers; use
incrementfor shared counts.
Lua¶
Game scripts save with store.save / store.load / store.add /
store.remove / store.keys (docs/SCRIPTING.md "Saving"). Each game
gets its own collection, lua.<game id>, so one game's scripts never see
another's. By default a game keeps them in save/scripts.db next to it
(ScriptModule::storeUrl, or KKE_SCRIPT_STORE=valkey://... for any
url above); ScriptModule::useStore hands scripts a store someone else
owns, such as a server's.
Backups¶
store->backup("backups/world-1.db") writes a copy of everything as
one SQLite file, to a temporary name first, so a copy that fails halfway
never replaces a good one. SQLite stores copy themselves with
VACUUM INTO (compact, consistent, while the game keeps using the
store); memory stores write their contents. Valkey and PostgreSQL are
backed up with their own tools (RDB snapshots, pg_dump):
canBackup() is false and backup() says so. kke_server makes them on a
schedule and keeps the newest few (docs/SERVER_HOSTING.md "Saves").
Servers¶
kke_server opens a store at start ("storage" in server.json,
KKE_SERVER_STORAGE, --storage); the default is
sqlite:<saveDir>/server.db. The console's status shows which. The
password in a postgres URL is shown as ***.
docker/server/docker-compose.yml has commented-out Valkey and Postgres
services next to the game server: uncomment one and set
KKE_SERVER_STORAGE to point at it.
Tests¶
tests/test_storage.cpp runs the same tests on every backend: memory and
SQLite always; Valkey and PostgreSQL when KKE_TEST_VALKEY /
KKE_TEST_POSTGRES name a server (backups too, for memory and SQLite), e.g.
docker run -d -p 6379:6379 valkey/valkey:8-alpine
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=kke -e POSTGRES_USER=kke postgres:17-alpine
KKE_TEST_VALKEY=valkey://127.0.0.1:6379 KKE_TEST_POSTGRES=postgres://kke:kke@127.0.0.1/kke ./bin/kke_tests --gtest_filter='*Store*'
All four passed that way on 2026-09-26 (Valkey 8, PostgreSQL 17).
Next¶
World saves on dedicated servers, once server scripts (#43) change a server's world (#45); a size budget per game collection.