STARK is a key-value based, B-tree implemented local database. It is specifically designed for mobile applications, desktop applications, and offline video games. With its speed, lightness, and easy syntax, STARK stands apart from other databases.
At its core, STARK is a C library with a C++ wrapper. It stores data on disk using a B-tree index and a flat data file, providing sub-microsecond lookups with a footprint of only 2–5 MB of RAM and under 1 MB on disk.
STARK works with .dat and .idx files to save data. It supports ACID properties, ensuring safe and reliable data persistence. After building STARK, you can add data via either the STARK CLI or directly through C++ code.
STARK uses a two-file storage model. When you open a database named "mydb", two files are created:
| File | Purpose |
|---|---|
mydb.idx | B-tree index — stores keys and pointers to data. Provides fast O(log n) lookups. |
mydb.dat | Data file — stores the actual values. Append-only with pointers from the index. |
The B-tree index keeps keys sorted, so range scans and existence checks are fast. The data file uses an append-only strategy, which makes writes safe and crash-resilient.
STARK supports three kinds of keys, each stored and optimized separately:
| Key Type | CLI Command Prefix | Use Case |
|---|---|---|
| Numeric keys (uint32) | addn, getn, deln, existsn | Integer IDs — player IDs, item IDs, save slots |
| String keys | adds, gets, dels, exist_str | Text-based lookups — usernames, config keys |
| Typed records | add, get | Structured data — game entities with fields |
STARK guarantees ACID compliance — the four properties that ensure database reliability even in the event of crashes, power failures, or errors.
Every transaction is all-or-nothing. Either all operations within a begin/commit block succeed, or none of them take effect.
Example: A player level-up that updates HP, level, and gold — all three changes are applied together or rolled back together.
Data always moves from one valid state to another. The B-tree index and data file are always in sync — no orphaned records or broken pointers.
How: The .idx and .dat files are updated in a coordinated manner. If the index points to data, the data exists.
Transactions don't interfere with each other. Each transaction sees a consistent snapshot. In practice, STARK's single-process model naturally provides isolation.
Note: STARK is designed for single-process use. For multi-process access, use file locking or a server-based database.
Once a commit completes, the data is permanently written to disk. Even if the system crashes or loses power, committed data survives.
How: Data is flushed to the .dat file before the commit returns. The sync() method forces an additional OS-level flush.
Why this matters for games: A player's progress must never be partially saved. If a power failure hits mid-save, STARK either fully applies the changes or leaves the database exactly as it was before.
Understanding the file format helps with backup strategies and debugging. You can safely back up both .idx and .dat files together. Never move or delete one without the other.
The STARK CLI lets you interactively create and manage databases without writing any code. Open or create a database by running stark_cli with a filename.
# Open or create a database in the current directory
stark_cli mygame
# If stark_cli is in your PATH, you can run it from anywhere:
cd ~/my_project/data
stark_cli savefile
# The prompt will change to show you're inside STARK:
stark> help
# Shows all available commands
When you first open a filename, STARK creates mygame.idx and mygame.dat in the current directory. If the files already exist, STARK opens and loads them.
| help | Display all available commands and their usage. Shows command syntax and descriptions. |
| exit | Save all pending changes to disk, close the database files, and quit the CLI. |
| stats | Show database statistics: total keys (numeric + string + typed), data file size, index file size, B-tree height, and page count. |
stark> stats
# Output:
# Keys: 15
# Data size: 2048 bytes
# Index pages: 1
# B-tree height: 2
Numeric keys are 32-bit unsigned integers (0 to 4,294,967,295). They are ideal for IDs — save slots, player IDs, inventory item IDs. Values can be strings or numbers, stored as raw bytes.
| addn <key> <value> | Insert or update a numeric key. If the key already exists, its value is overwritten. Value can be quoted strings or plain numbers. |
| getn <key> | Retrieve the value for a numeric key. Returns the value as a string, or "key not found" if the key doesn't exist. |
| deln <key> | Delete a numeric key and its associated value. Silently succeeds if the key doesn't exist. |
| existsn <key> | Check whether a numeric key exists. Returns true or false. |
# Store a save slot
stark> addn 0 "slot_empty"
stark> addn 1 "Hero - Level 12"
stark> addn 999 "1000 gold coins"
# Read them back
stark> getn 1
# Output: Hero - Level 12
stark> getn 5
# Output: key not found
# Check existence
stark> existsn 1
# Output: true
stark> existsn 5
# Output: false
# Delete a key
stark> deln 0
stark> existsn 0
# Output: false
# Overwrite a key (same as add)
stark> addn 1 "Hero - Level 15"
stark> getn 1
# Output: Hero - Level 15
String keys let you use text-based lookups. The string is hashed internally for storage, but you interact with it by its original text. This is useful for config keys, usernames, or named settings.
| adds <key> <value> | Insert or update a string key. Both key and value are quoted strings. Overwrites if the key already exists. |
| gets <key> | Retrieve the value for a string key. Returns the value or "key not found". |
| dels <key> | Delete a string key and its value. Silently succeeds if the key doesn't exist. |
| exist_str <key> | Check whether a string key exists. Returns true or false. |
# Store settings
stark> adds "username" "player_one"
stark> adds "theme" "dark"
stark> adds "music_volume" "85"
# Read them back
stark> gets "username"
# Output: player_one
stark> gets "fullscreen"
# Output: key not found
# Check and delete
stark> exist_str "theme"
# Output: true
stark> dels "theme"
Types let you define structured records — like a lightweight schema. Each type has named fields with fixed data types. This is how you store game entities, inventory items, or any multi-field data.
| Type Keyword | C/C++ Constant | Size | Notes |
|---|---|---|---|
int | TYPE_INT (1) | 4 bytes | Signed 32-bit integer |
string(N) | TYPE_STRING (2) | N+1 bytes | N = max characters. Includes null terminator. Use string(32), string(255), etc. |
name string(32), STARK allocates exactly 33 bytes per record (32 chars + null terminator). Smaller sizes save memory and disk space. Use string(255) as a safe default if you're unsure.
| define <name> { field1 type1 field2 type2 ... } | Define a new type. Each field has a name and a type (int or string(N)). If the type already exists, this fails. |
| undefine <name> | Delete a type definition and all records of that type. This is irreversible. |
| desc <name> | Show a type's definition — all fields with their names, types, and sizes. |
# Define a player type
stark> define player {
name string(32)
hp int
level int
gold int
class string(16)
}
# View the type definition
stark> desc player
# Output:
# Type: player
# Fields:
# name = string(32)
# hp = int
# level = int
# gold = int
# class = string(16)
# Define an item type
stark> define item {
name string(64)
damage int
type string(16)
}
# Remove a type (deletes all records too)
stark> undefine item
Once a type is defined, you can add, retrieve, and update records. Each record is identified by a numeric key and contains field values in field=value format.
| add <type> <key> field1=val1 field2=val2 ... | Add or update a record. The type must exist. String values should be quoted. Omitting a field sets it to zero/empty. |
| get <type> <key> | Retrieve a record. Returns all field values in field=value format, or "type:key not found". |
# Add players (keys 1 and 2)
stark> add player 1 name="Hero" hp=100 level=5 gold=250 class="warrior"
stark> add player 2 name="Mage" hp=80 level=7 gold=450 class="wizard"
# Get a player
stark> get player 1
# Output: name="Hero" hp=100 level=5 gold=250 class="warrior"
# Non-existent record
stark> get player 3
# Output: player:3 not found
# Update a record (same key overwrites all fields)
stark> add player 1 name="Hero" hp=85 level=6 gold=300 class="warrior"
# Verify update
stark> get player 1
# Output: name="Hero" hp=85 level=6 gold=300 class="warrior"
# Check stats after adding data
stark> stats
# Output shows: total keys, data file size, index size, etc.
add with an existing key, all fields are overwritten. STARK does not support updating a single field — you must provide the complete record each time.
Transactions group multiple operations into a single atomic unit. This is critical when you need to update multiple values that must stay consistent — like a player winning a fight and gaining both XP and loot.
| begin | Start a new transaction. All subsequent writes (addn, adds, add) are buffered until commit or rollback. |
| commit | Commit the transaction — all buffered changes are written to disk atomically. If the system crashes during commit, the database rolls back to the pre-transaction state. |
| rollback | Discard all changes made since begin. The database reverts to exactly the state it was in before the transaction started. |
# Setup a player
stark> define player { id int hp int level int gold int }
stark> add player 1 id=123 hp=100 level=0 gold=50
# === SUCCESSFUL TRANSACTION ===
# Player wins a fight — update atomically
stark> begin
stark> add player 1 id=123 hp=45 level=1 gold=150
stark> commit
# All changes saved atomically
# === ROLLBACK EXAMPLE ===
# Suppose a transaction goes wrong
stark> begin
stark> add player 1 id=123 hp=0 level=0 gold=0
stark> rollback
# Player data restored to: hp=45, level=1, gold=150
# Verify rollback worked
stark> get player 1
# Output: id=123 hp=45 level=1 gold=150
Best practice: Always wrap related changes in a transaction. If you're updating a player's HP, XP, and inventory after a battle, put all three updates in one
begin/commitblock.
This walkthrough shows how to use STARK to build a complete game save system. Follow along step by step.
# ================================================
# STEP 1: Open the database
# ================================================
stark_cli mygame
stark>
# ================================================
# STEP 2: Define types for your game
# ================================================
# Define the player type
stark> define player {
name string(32)
hp int
max_hp int
level int
xp int
gold int
class string(16)
}
# Define the inventory type
stark> define item {
name string(64)
damage int
defense int
type string(16)
# type: weapon, armor, potion, quest
}
# ================================================
# STEP 3: Store save data
# ================================================
# Save the main character (key = 1)
stark> add player 1 name="Aldric" hp=120 max_hp=120 level=8 xp=4500 gold=1250 class="warrior"
# Save inventory items (keys 1-3)
stark> add item 1 name="Iron Sword" damage=15 defense=0 type="weapon"
stark> add item 2 name="Steel Shield" damage=0 defense=20 type="armor"
stark> add item 3 name="Health Potion" damage=0 defense=0 type="potion"
# Store save metadata using string keys
stark> adds "save_version" "1.0"
stark> adds "play_time" "04:23:15"
stark> adds "chapter" "3"
# Use numeric keys for save slots
stark> addn 1 "Aldric - Level 8 - Chapter 3"
stark> addn 2 "Empty Slot"
stark> addn 3 "Empty Slot"
# ================================================
# STEP 4: Use a transaction for battle updates
# ================================================
# Player fights a dragon — wins but takes damage
stark> begin
# Player takes 40 damage
stark> add player 1 name="Aldric" hp=80 max_hp=120 level=8 xp=4500 gold=1250 class="warrior"
# Player gains 500 XP and 200 gold from the kill
stark> adds "dragon_kills" "1"
# Commit — all changes saved together
stark> commit
# ================================================
# STEP 5: Verify your save data
# ================================================
stark> get player 1
# Output: name="Aldric" hp=80 max_hp=120 level=8 xp=4500 gold=1250 class="warrior"
stark> get item 1
# Output: name="Iron Sword" damage=15 defense=0 type="weapon"
stark> gets "chapter"
# Output: 3
stark> desc player
# Shows all player fields with types
stark> stats
# Shows total keys, file sizes, B-tree info
# ================================================
# STEP 6: Save and exit
# ================================================
stark> exit
# All data persisted to mygame.idx and mygame.dat
The C++ wrapper (stark::Database) provides a modern, exception-safe interface over the C API. After installation, just #include <stark.hpp> and link with -lstark.
// main.cpp
#include <stark.hpp>
#include <iostream>
int main() {
// Open database (creates "save.idx" and "save.dat")
stark::Database db("save");
// Save data
db.add(1, "Hero");
db.add(2, "100 HP");
// Load data
std::cout << db.get(1) << std::endl; // Prints: Hero
// Get with default value
std::string val = db.get(99, "not found"); // "not found"
return 0;
}
// Open existing or create new
stark::Database db("mydb");
// Throws stark::Error if
// the database cannot be opened
// Move is supported
stark::Database db2 = std::move(db);
// Copy is deleted
// Database db3 = db2; // ERROR
// Destructor auto-syncs
// and closes the database
The database automatically calls sync() and stark_close() in the destructor. You don't need to manually close it. If you need explicit control, call db.sync() to flush changes to disk.
All numeric key operations throw stark::Error on failure. get() returns an empty string if the key is not found.
// Insert or update (key is uint32_t)
db.add(1, "Hero");
db.add(42, "anything");
// Get — returns "" if not found
std::string v = db.get(1);
// Get with default fallback
std::string v2 = db.get(99,
"default value");
// Remove — returns true if deleted,
// false if key didn't exist
bool removed = db.remove(1);
// Check existence
if (db.exists(1)) {
// key exists
}
String keys use std::string arguments. Internally, keys are hashed, but you interact with them using the original text.
// Insert or update
db.put_str("username", "player_one");
db.put_str("theme", "dark");
// Get — returns "" if not found
std::string v = db.get_str(
"username");
// Remove by string key
bool ok = db.remove_str(
"username");
// Check existence
if (db.exists_str("theme")) {
// key exists
}
Define schemas with stark::Field objects, then add and retrieve structured records. Field type constants: TYPE_INT (1) and TYPE_STRING (2).
using namespace stark;
// Define a type with fields
db.define_type("player", {
Field("name", TYPE_STRING, 32),
Field("hp", TYPE_INT),
Field("level", TYPE_INT),
});
// Remove a type (returns
// true if it existed)
bool removed = db.undefine_type(
"player");
// Add a typed record
db.add_typed("player", 1,
"name=Hero hp=100 level=5");
// Get typed record (returns "" if
// not found)
std::string v = db.get_typed(
"player", 1);
// Describe type (returns vector
// of Field objects)
auto fields = db.describe_type(
"player");
Field struct:
Field(name, type, size)—nameis the field name,typeisTYPE_INTorTYPE_STRING,sizeis the string buffer size (default 4, ignored for int).
// Start transaction
db.begin();
// Make changes
db.add(1, "updated");
db.put_str("key", "val");
// Commit (atomic write)
db.commit();
// Or rollback (discard)
// db.rollback();
// Check if in transaction
if (db.in_transaction()) {
// inside begin/commit block
}
The C API provides a cursor for iterating over all keys in order. The C++ wrapper does not yet expose this directly, but you can use it via the C functions by linking to stark.h.
stark_cursor_create(db) | Create a new cursor for the database. Returns a cursor handle or NULL on error. |
stark_cursor_first(cur) | Move cursor to the first (smallest) key. Returns STARK_OK or STARK_NOT_FOUND if empty. |
stark_cursor_last(cur) | Move cursor to the last (largest) key. Returns STARK_OK or STARK_NOT_FOUND if empty. |
stark_cursor_next(cur) | Move cursor to the next key. Returns STARK_OK or STARK_NOT_FOUND if at end. |
stark_cursor_prev(cur) | Move cursor to the previous key. Returns STARK_OK or STARK_NOT_FOUND if at start. |
stark_cursor_get(cur, &key, buf, &size) | Get the current key and value at the cursor position. |
stark_cursor_destroy(cur) | Free the cursor. Always call this when done. |
// C API cursor example
#include <stark.h>
stark_db_t* db = stark_open("mydb", 0);
stark_cursor_t* cur = stark_cursor_create(db);
if (stark_cursor_first(cur) == STARK_OK) {
do {
uint32_t key;
char buf[4096];
size_t size = sizeof(buf);
stark_cursor_get(cur, &key, buf, &size);
printf("key=%u value=%s\n", key, buf);
} while (stark_cursor_next(cur) == STARK_OK);
}
stark_cursor_destroy(cur);
stark_close(db);
The C API provides direct access to all STARK functionality. Include <stark.h> and link with -lstark.
// main.c
#include <stark.h>
#include <stdio.h>
int main() {
// Open database
stark_db_t* db = stark_open("save", 0);
if (!db) {
printf("Failed to open database\n");
return 1;
}
// Add a key
stark_add(db, 1, "Hero");
// Get the value
char buf[256];
size_t size = sizeof(buf);
stark_get(db, 1, buf, &size);
printf("Value: %s\n", buf);
// Close database
stark_close(db);
return 0;
}
All C API functions return a result code indicating success or the type of error that occurred.
| Constant | Value | Description |
|---|---|---|
STARK_OK | 0 | Operation succeeded. |
STARK_ERR | 1 | Generic error. |
STARK_NOT_FOUND | 2 | Key or record not found. |
STARK_INVALID_KEY | 3 | Invalid key format or size. |
STARK_FULL | 4 | Database file is full. |
STARK_CORRUPT | 5 | Database file is corrupted. |
STARK_NO_MEM | 6 | Out of memory. |
STARK is designed for raw speed and minimal resource usage. Here are benchmark comparisons with popular databases.
| Operation | STARK | SQLite | Redis | MongoDB |
|---|---|---|---|---|
| Write 1 record | 0.8 µs | 5 µs | 1 µs | 50 µs |
| Read 1 record | 0.5 µs | 3 µs | 0.8 µs | 30 µs |
| Write 1,000 records | 0.8 ms | 5 ms | 1 ms | 50 ms |
| Read 1,000 records | 0.5 ms | 3 ms | 0.8 ms | 30 ms |
STARK's B-tree implementation provides O(log n) lookups with minimal disk I/O, making it ideal for game save systems and local storage.