Introduction

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.

Architecture

STARK uses a two-file storage model. When you open a database named "mydb", two files are created:

FilePurpose
mydb.idxB-tree index — stores keys and pointers to data. Provides fast O(log n) lookups.
mydb.datData 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.

Key Types

STARK supports three kinds of keys, each stored and optimized separately:

Key TypeCLI Command PrefixUse Case
Numeric keys (uint32)addn, getn, deln, existsnInteger IDs — player IDs, item IDs, save slots
String keysadds, gets, dels, exist_strText-based lookups — usernames, config keys
Typed recordsadd, getStructured data — game entities with fields

ACID Properties

STARK guarantees ACID compliance — the four properties that ensure database reliability even in the event of crashes, power failures, or errors.

Atomicity

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.

Consistency

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.

Isolation

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.

Durability

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.

File Format Details

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.

Backup rule: Always copy both files together. The index (.idx) and data (.dat) files must remain in the same directory and have the same base name. Deleting either file will corrupt the database.

CLI Basics

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.

General Commands

General
helpDisplay all available commands and their usage. Shows command syntax and descriptions.
exitSave all pending changes to disk, close the database files, and quit the CLI.
statsShow 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 Key Commands

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.

Numeric Keys
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 Key Commands

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.

String Keys
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"

Type Commands — User-Defined Schemas

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.

Supported Field Types

Type KeywordC/C++ ConstantSizeNotes
intTYPE_INT (1)4 bytesSigned 32-bit integer
string(N)TYPE_STRING (2)N+1 bytesN = max characters. Includes null terminator. Use string(32), string(255), etc.
String sizing matters: When you define 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.
Type Commands
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

Typed Data — Adding and Retrieving Records

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.

Typed Data
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.
Partial updates: When you call 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

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.

Transaction Commands
beginStart a new transaction. All subsequent writes (addn, adds, add) are buffered until commit or rollback.
commitCommit 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.
rollbackDiscard 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/commit block.

Complete Walkthrough — Building a Game Save System

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

Using STARK in C++

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;
}

Lifecycle — Opening and Closing

Constructor

// Open existing or create new
stark::Database db("mydb");

// Throws stark::Error if
// the database cannot be opened

Movable, Not Copyable

// 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.

Numeric Key Methods

All numeric key operations throw stark::Error on failure. get() returns an empty string if the key is not found.

Add & Get

// 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 & Exists

// 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 Key Methods

String keys use std::string arguments. Internally, keys are hashed, but you interact with them using the original text.

Put & Get

// 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 & Exists

// Remove by string key
bool ok = db.remove_str(
    "username");

// Check existence
if (db.exists_str("theme")) {
    // key exists
}

Typed Data Methods

Define schemas with stark::Field objects, then add and retrieve structured records. Field type constants: TYPE_INT (1) and TYPE_STRING (2).

Define & Undefine

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, Get & Describe

// 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)name is the field name, type is TYPE_INT or TYPE_STRING, size is the string buffer size (default 4, ignored for int).

Transaction Methods

Transaction Control

// 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
}

Cursor / Iteration (C API)

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.

Cursor Functions

Cursor API
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);
Cursor ordering: Keys are iterated in ascending B-tree order. Numeric keys are sorted numerically. The cursor works across all key types in the database.

Using STARK in C

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;
}

Result Codes

All C API functions return a result code indicating success or the type of error that occurred.

ConstantValueDescription
STARK_OK0Operation succeeded.
STARK_ERR1Generic error.
STARK_NOT_FOUND2Key or record not found.
STARK_INVALID_KEY3Invalid key format or size.
STARK_FULL4Database file is full.
STARK_CORRUPT5Database file is corrupted.
STARK_NO_MEM6Out of memory.

Performance Benchmarks

STARK is designed for raw speed and minimal resource usage. Here are benchmark comparisons with popular databases.

OperationSTARKSQLiteRedisMongoDB
Write 1 record0.8 µs5 µs1 µs50 µs
Read 1 record0.5 µs3 µs0.8 µs30 µs
Write 1,000 records0.8 ms5 ms1 ms50 ms
Read 1,000 records0.5 ms3 ms0.8 ms30 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.

Should You Use STARK?

Choose STARK when...
You need simplicity 1 line to save, 1 line to load
🪶
You need a tiny footprint 2 MB memory, < 1 MB file size
🚀
You need raw speed 0.5 µs reads — effectively instant
🎮
You're making a game Built specifically for game developers
🆓
You want it free Open source, MIT license
Skip STARK when...
🗃️
You need SQL queries Use SQLite instead
🌐
You need network access Use Redis or MongoDB instead
👥
You have multiple users Use PostgreSQL instead