Arkilian mark

Documentation

Use Arkilian
in your language.

One embedded engine, six SDKs. Open a database, run DDL through it, and every write streams to S3 — verified, snapshotted, replayable.

Start with Quickstart
On this page

01 · Start

Quickstart #

Use Arkilian anywhere you use SQLite. Open a database, run DDL through it, and every write is captured, batched, and streamed to S3 — with no code changes beyond the constructor.

app.js
import Arkilian from 'arkilian';

const db = new Arkilian('app.sqlite');

db.exec(`CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL
)`);

db.run('INSERT INTO users (name) VALUES (?)', ['Ada']);
console.log(db.all('SELECT * FROM users'));

db.close();

Run DDL through Arkilian — not a raw handle — so capture triggers are wired automatically.

Run DDL through Arkilian. Raw handles bypass trigger wiring — use resyncTriggers() afterwards, or enable auto-resync.

Tables without a PRIMARY KEY are skipped; count them with backupSkippedTableCount.

02 · Start

Configuration #

Set ARKILIAN_ENABLE_BACKUP=0 to run embedded-only with no S3 traffic. Otherwise Arkilian reads every variable below from the process environment or a ./.env file — environment wins.

terminal
npm install arkilian        # Node.js / Bun — prebuilt N-API binary, no compiler needed
pip install arkilian        # Python (CFFI)
go get github.com/arkiliandb/arkilian/bindings/go/arkilian
VariableDefault — behavior
ARKILIAN_DB_PATHapp.sqlite — path to the primary database file
ARKILIAN_BACKUP_PATHbackup.sqlite — staging path for snapshot copies
ARKILIAN_BACKUP_INTERVAL3600 — snapshot interval in seconds (minimum 1)
ARKILIAN_CHUNK_INTERVAL_SEC1 — seconds between packaging outbox rows into S3 chunks
ARKILIAN_MANIFEST_INTERVAL_SEC30 — minimum seconds between manifest publishes
ARKILIAN_S3_ENDPOINT(empty) — S3 URL. Unset means shipping stays dormant
ARKILIAN_S3_BUCKET(empty) — target bucket for snapshots, chunks, manifests
ARKILIAN_S3_REGIONus-east-1 — SigV4 signing region
ARKILIAN_S3_PREFIXdb_default — isolates one database inside a shared bucket
ARKILIAN_MANIFEST_HMAC_KEY(empty) — REQUIRED for publishing and hydration. Fail closed
ARKILIAN_MAX_QUEUE_DEPTH100000 — outbox rows before capture pauses (writes never block)
ARKILIAN_MAX_ATTEMPTS100 — upload retries per chunk before rows move to the DLQ
ARKILIAN_ENABLE_BACKUP1 — set 0 to disable outbound shipping at runtime
ARKILIAN_OUTBOX_DURABLE1 — FULL synchronous outbox flush. Set 0 (NORMAL) for max throughput

03 · Start

For AI agents #

Using a coding agent? Copy the instructions below and paste them into its chat. They tell the agent how to swap SQLite for Arkilian, configure S3, avoid the common pitfalls, and prove replication works before it reports back.

Plain Markdown · 153 lines · paste into any coding agent

# Task: integrate Arkilian into this project

Arkilian is an embedded SQLite database with built-in replication. Your code keeps using ordinary SQL. Arkilian captures every write and streams it to S3-compatible object storage (SHA-256-verified chunks plus periodic snapshots), so a new machine can restore the database from the bucket.

Your job: replace this project's SQLite access with Arkilian, configure storage, and prove replication works. Reference docs: https://arkilian.com/documentation

## Ground rules (read first)
1. Make the smallest change that works. Do not rewrite unrelated code. Do not change the database schema unless a step below says so.
2. If a step fails, or this document disagrees with what you see (package not found, different method names, unsupported OS), STOP and tell the user exactly what happened. Do not improvise: do not swap in another database library, invent credentials, fake a verification result, or skip a step.
3. Secrets (S3 keys, HMAC key) live only in .env or the environment. Never hardcode, print, log or commit them. Ask the user for the real values.
4. Before the first run against an existing database file, copy it (for example: cp app.sqlite app.sqlite.bak). Opening it with Arkilian adds capture triggers (named trg_...) and internal tables (_pending_backup, _dead_backup, _arkilian_meta).

## Step 1. Inspect before changing anything
Find and write down:
a) the language and package manager;
b) every place SQLite is opened (better-sqlite3, sqlite3, database/sql with a SQLite driver, rusqlite, PDO, ORMs such as Prisma, Drizzle, TypeORM, SQLAlchemy);
c) every place the schema is created or migrated;
d) every table that has no PRIMARY KEY;
e) whether more than one process opens the same database file (cluster mode, multiple workers, per-request processes).
Then give the user a plan in 3 to 5 lines and continue.

## Step 2. Install (use ONLY the entry for your language)
- Node.js / Bun: npm install arkilian
  THEN ALWAYS check that the native binary loads: node --input-type=module -e "import 'arkilian'"
  Prebuilt binaries exist for Linux x64 and arm64, Alpine x64, macOS x64/arm64 and Windows x64. The Linux (glibc) ones need BOTH glibc 2.34+ AND a libstdc++ that provides GLIBCXX_3.4.31 (GCC 13 or newer): Ubuntu 24.04, Debian 13 and Fedora 38+ qualify. Ubuntu 22.04 does NOT (confirmed: it fails to load), and Debian 12 (the default node:20 and node:22 Docker images) and Amazon Linux 2023 ship an older libstdc++ and will likely fail the same way. On those systems "npm install" still SUCCEEDS and the error only appears at import.
  If the import check fails with an error that mentions GLIBCXX or GLIBC, compile from source instead (needs g++, make, python3 and libcurl4-openssl-dev installed):
    rm -rf node_modules/arkilian && ARKILIAN_SKIP_DOWNLOAD=1 npm install arkilian
  then run the import check again. In a Dockerfile, install those build packages before "npm install" (or use a base image with GCC 13+, such as ubuntu:24.04) and set ARKILIAN_SKIP_DOWNLOAD=1 so the image always builds from source.
  Linux also needs libcurl at runtime (apt install libcurl4). Other platforms (for example Alpine arm64) compile from source and need a C compiler plus libcurl headers.
- Python: do NOT run "pip install arkilian" (the PyPI package is an outdated stub with no native library). Install the release wheel instead:
  pip install https://github.com/arkiliandb/Arkilian/releases/download/v1.6.3/arkilian-1.6.3-py3-none-any.whl
  It bundles a Linux x86-64 library that needs glibc 2.34+ (Ubuntu 22.04 and newer) and libcurl. On macOS, Windows, ARM or an older Linux, STOP and ask the user.
- Go: "go get" does not work for this module (it relies on symlinks into the repo). Clone https://github.com/arkiliandb/Arkilian next to this project, then add to go.mod:
  require github.com/arkiliandb/Arkilian/bindings/go v1.6.3
  replace github.com/arkiliandb/Arkilian/bindings/go => ../Arkilian/bindings/go
  Needs cgo (a C compiler) and libcurl development headers.
- Rust: do NOT use the crates.io package (outdated 1.0.0). Clone the repo and depend on it by path:
  arkilian = { path = "../Arkilian/bindings/rust/arkilian" }
  Needs libcurl headers and libclang.
- PHP (7.4+, ext-ffi enabled): copy bindings/php/Arkilian.php from the repo, take libarkilian.so (or .dylib) from the C release archive, and set ARKILIAN_LIB_PATH to its full path.
- C / C++: download arkilian-c-v1.6.3-<os>-<arch> (.tar.gz, or .zip on Windows) from https://github.com/arkiliandb/Arkilian/releases. It contains include/arkilian/*.h and lib/libarkilian.so and libarkilian.a (the Linux build needs glibc 2.34+ and libcurl). Compile with -I<dir>/include/arkilian and link -larkilian -lcurl -lpthread -lm.

## Step 3. Change the code
Open the database ONCE at startup and reuse that single handle everywhere. Keep your SQL exactly as it is.

Node.js (ES modules; from CommonJS use: const { default: Arkilian } = await import('arkilian')). All calls are synchronous, so do not add await:
```js
import Arkilian from 'arkilian';
const db = new Arkilian('app.sqlite');
db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
db.run('INSERT INTO users (name) VALUES (?)', ['Ada']);   // write; params are an array
const rows = db.all('SELECT * FROM users WHERE id = ?', [1]);  // array of plain objects
const first = rows[0];                                     // "get one row"
db.transaction((tx) => {                                   // rolls back if the callback throws
  tx.run('INSERT INTO users (name) VALUES (?)', ['Bob']);
});
db.close();                                                // on shutdown
```
If the project used better-sqlite3: db.prepare(sql).run(a, b) becomes db.run(sql, [a, b]); .all(a) becomes db.all(sql, [a]); .get(a) becomes db.all(sql, [a])[0].

Python (rows come back as dicts, NOT tuples: change row[0] to row["column_name"]; each run() commits immediately unless you call begin() first):
```python
from arkilian import Arkilian
db = Arkilian("app.sqlite")
db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
db.run("INSERT INTO users (name) VALUES (?)", ["Ada"])
rows = db.all("SELECT * FROM users WHERE id = ?", [1])   # list of dicts
db.begin()
try:
    db.run("INSERT INTO users (name) VALUES (?)", ["Bob"])
    db.commit()
except Exception:
    db.rollback()
    raise
db.close()
```

Go (Exec takes plain SQL with no ? parameters; use Prepare for parameterized writes and Query for reads):
```go
import "github.com/arkiliandb/Arkilian/bindings/go/arkilian"

db, err := arkilian.OpenDB("app.sqlite")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
stmt, err := db.Prepare("INSERT INTO users (name) VALUES (?)")
err = stmt.BindText(1, "Ada")        // parameter numbers start at 1
_, err = stmt.Step()
err = stmt.Finalize()
rows, err := db.Query("SELECT * FROM users WHERE id = ?", 1)   // []map[string]interface{}
// transactions: db.Begin(), db.Commit(), db.Rollback()
```
Check every err value; the lines above omit that for brevity.

Rust: use arkilian::Database; let db = Database::new("app.sqlite")?; db.exec(sql)?; db.prepare(sql)?; db.use_stmt(0)?; db.bind_text(1, "x")?; db.bind_int(2, 5)?; db.step(); db.finalize()?; db.begin()? / db.commit()? / db.rollback()?
PHP: require_once 'Arkilian.php'; $db = new Arkilian('app.sqlite'); $db->exec($sql); $db->run($sql, [$a]); $rows = $db->all($sql, [$a]); $db->begin(); $db->commit(); $db->rollback(); $db->close();
C: arkilian *db = NULL; db_init(&db, "app.sqlite"); db_exec(db, sql); db_prepare(db, sql); db_bind_text(db, 1, "x"); db_bind_int(db, 2, 5); db_step(db); db_finalize(db); db_close(db);

## Step 4. Rules (do not skip any)
1. Run ALL DDL (CREATE, ALTER, DROP and every migration) through the Arkilian handle, never through a raw SQLite handle or a separate ORM connection. If a migration tool opens its own connection, route its SQL through Arkilian. If that is impossible, call resync after every migration: Node db.resyncTriggers(), Python db.resync_triggers(), Go db.ResyncTriggers(), Rust db.resync_triggers(), PHP $db->resyncTriggers(), C db_resync_triggers(db).
2. Every table you want replicated needs a PRIMARY KEY. A table without one is NOT replicated and the only sign is a warning in the log. Do not change the schema yourself; list those tables for the user.
3. One process per database file, one handle per process. Arkilian runs background threads, so open it once at startup and keep it open. Never open and close it per request. For multi-worker servers (PM2/cluster, gunicorn/uvicorn workers, php-fpm) or short-lived processes (serverless, CLI scripts), STOP and ask the user how the database should be owned.
4. One writer per S3 prefix. Give each database or tenant its own ARKILIAN_S3_PREFIX. A second live writer on the same prefix (for example a staging copy using the production prefix) freezes shipping.
5. Never read, edit or drop _pending_backup, _dead_backup, _arkilian_meta or any trigger whose name starts with trg_.
6. Call close() on graceful shutdown. It flushes and publishes the last chunks (up to 10 seconds by default).
7. In C, never share one handle across threads.

## Step 5. Configure storage
Settings come from the real environment or a .env file in the process WORKING DIRECTORY (not the database folder). The real environment wins. Required for replication:
```
ARKILIAN_S3_ENDPOINT=https://s3.amazonaws.com
ARKILIAN_S3_BUCKET=<bucket>
ARKILIAN_S3_REGION=us-east-1
ARKILIAN_S3_ACCESS_KEY=<access key>
ARKILIAN_S3_SECRET_KEY=<secret key>
ARKILIAN_S3_PREFIX=<one unique prefix per database>
ARKILIAN_MANIFEST_HMAC_KEY=<long random secret>
```
- Generate the HMAC key with: openssl rand -hex 32. It is REQUIRED: without it nothing is published and nothing can be restored. Keep a copy: every machine that restores this database needs the same key.
- The endpoint must be https://. Plain http:// is accepted only for localhost or private-network addresses (for example a local MinIO). Otherwise backup is disabled with an error in the log; ARKILIAN_ALLOW_INSECURE=1 overrides this for local development only.
- If the log says "storage endpoint refused ... SSRF guard", add the endpoint host to ARKILIAN_STORAGE_HOSTS (comma-separated).
- Replication is ON by default. If you have no storage yet (local development, tests, CI), set ARKILIAN_ENABLE_BACKUP=0. With backup on but settings incomplete, Arkilian only logs a warning and changes pile up in a local queue.
- Add .env to .gitignore and commit a .env.example containing the variable names with empty values.

## Step 6. Restore on a new machine (ONLY if the user asked for restore or failover)
Skip this step otherwise. On the very first deployment the prefix is empty, so there is nothing to restore and the call fails; ask the user how first start should behave.
- Call restore BEFORE any handle is opened, and only when the local database file does not exist.
- It needs ARKILIAN_MANIFEST_HMAC_KEY (the same key the writer used). It refuses tampered or unsigned data and refuses if the local file is newer than the snapshot. Never catch and ignore its errors.
- Node: Arkilian.hydrateS3('app.sqlite', { endpoint, bucket, region, accessKey, secretKey, prefix })  // all but region required
- Python: Arkilian.hydrate_s3(db_path, endpoint, bucket, region, access_key, secret_key, prefix)
- Go: err := arkilian.HydrateS3("app.sqlite", arkilian.S3Config{Endpoint: ..., Bucket: ..., Region: ..., AccessKey: ..., SecretKey: ..., Prefix: ...})
- Rust: arkilian::hydrate_s3(path, prefix, &s3config)
- PHP: Arkilian::hydrateS3($path, $prefix, ['endpoint' => ..., 'bucket' => ..., 'region' => ..., 'accessKey' => ..., 'secretKey' => ...])
- C: arkilian_hydrate_s3(path, endpoint, bucket, region, access_key, secret_key, prefix, NULL, NULL) returns 0 on success, a negative number on failure.
Then open the database normally.

## Step 7. Verify (do both parts, in order)
Part A, no storage: set ARKILIAN_ENABLE_BACKUP=0 and run the project's tests and start the app. They must behave exactly as before. In this mode the health value is false and the queue count grows; that is expected, so do not assert on them.
Part B, with real storage (ask the user for a test bucket or prefix):
1. Start the app and perform one write through Arkilian.
2. Poll for up to 60 seconds (chunks ship about every second, the manifest at most every 30 seconds) until ALL are true: queue depth is 0, chunk count is 1 or more, health is true.
   Node: db.backupQueueDepth, db.backupChunkCount, db.backupHealthy
   Python: db.backup_queue_depth, db.backup_chunk_count, db.backup_healthy
   Go: db.BackupQueueDepth(), db.BackupChunkCount(), db.BackupIsHealthy()
   Rust: db.backup_queue_depth(), db.backup_is_healthy()    PHP: $db->backupQueueDepth(), $db->backupChunkCount(), $db->backupIsHealthy()
   C: db_backup_queue_depth(db), db_backup_chunk_count(db), db_backup_is_healthy(db)
3. Also confirm skipped-table count and dead-letter count are both 0 (Node backupSkippedTableCount and backupDeadLetterCount; Python backup_skipped_table_count and backup_dead_letter_count; Go BackupSkippedTableCount() and BackupDeadLetterCount()).
4. If the aws CLI is available, list s3://<bucket>/<prefix>/ and confirm manifest.json and manifest.sig exist.
If any check fails or you could not run Part B, say so plainly. Never claim replication works unless you observed these results.

## Step 8. Report back
Tell the user: the files you changed; the environment variables they still must set; any tables skipped for lacking a PRIMARY KEY; any ground rule or step you stopped on; and which verification parts you actually ran.
Credentials stay with you. The instructions tell the agent to ask for S3 keys and the manifest HMAC key, keep them in .env, and never commit them.

04 · Core

Capture & snapshots #

DDL executed through Arkilian creates row-level triggers for every table with a primary key, backed by a transaction-aware sidecar journal. Each INSERT, UPDATE, and DELETE lands in the local _pending_backup outbox in strict LSN order.

Chunks — every ARKILIAN_CHUNK_INTERVAL_SEC a flush thread batches outbox rows into SHA-256-verified SQL chunks, uploaded with SigV4 presigned PUTs. Delivery is at-least-once: replay uses idempotent REPLACE INTO / DELETE, so overlapping ranges are safe.

Snapshots — every ARKILIAN_BACKUP_INTERVAL a snapshot thread takes a non-blocking online backup (sqlite3_backup_step) to backup.sqlite. If the outbox ever hits capacity, capture pauses while application writes proceed — check capturePaused and take a fresh snapshot to close the gap.

05 · Core

Hydration #

Cold-start any replica from object storage before opening it. Hydration downloads the latest snapshot, verifies every digest and the HMAC manifest signature, then replays incremental chunks. Missing or mismatched signatures refuse to restore.

app.js
import Arkilian from 'arkilian';

Arkilian.hydrateS3('app.sqlite', {
  endpoint: 'https://s3.amazonaws.com',
  bucket: 'my-app-backups',
  region: 'us-east-1',
  accessKey: process.env.ARKILIAN_S3_ACCESS_KEY,
  secretKey: process.env.ARKILIAN_S3_SECRET_KEY,
  prefix: 'tenant-production',
});

const db = new Arkilian('app.sqlite');
Fail closed by design. Hydration refuses unsigned or mismatched manifests — there is no insecure fallback.

Requires ARKILIAN_MANIFEST_HMAC_KEY in the environment — the same key that signed the manifest.

06 · SDKs

Node.js / Bun #

Install arkilian from npm. The postinstall pulls a prebuilt N-API binary for your platform (Linux x64/arm64 glibc + musl, macOS x64/arm64, Windows x64) and falls back to building from bundled C sources offline.

terminal
npm install arkilian

Writes, reads, transactions

app.js
import Arkilian from 'arkilian';
const db = new Arkilian('app.sqlite');

db.exec('CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, item TEXT, qty INT)');
db.run('INSERT INTO orders (item, qty) VALUES (?, ?)', ['Widget', 5]);

console.log(db.lastInsertRowid); // id of the row just written
console.log(db.changes);         // rows modified by the last statement

const orders = db.all('SELECT id, item, qty FROM orders');

db.transaction((tx) => {
  tx.run('INSERT INTO orders (item, qty) VALUES (?, ?)', ['Gadget', 10]);
  tx.run('INSERT INTO orders (item, qty) VALUES (?, ?)', ['Sprocket', 2]);
}); // rolls back automatically on throw

Backup health

health.js
console.log(db.backupHealthy);              // 1 when all core checks pass
console.log(db.backupHealthFlags.toString(2)); // ARK_HF_* bitmask — exact fault
console.log(db.backupQueueDepth);              // rows waiting in _pending_backup
console.log(db.backupOldestPendingAgeSec);     // real-time replication lag
console.log(db.backupChunkCount);              // chunks shipped so far

db.close();

07 · SDKs

Python #

Install from bindings/python. The binding is an idiomatic class over CFFI — rows come back as lists of dicts.

terminal
cd bindings/python
pip install -e .
app.py
from arkilian import Arkilian

db = Arkilian("app.sqlite")
db.exec("CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, item TEXT, qty INT)")

db.run("INSERT INTO orders (item, qty) VALUES (?, ?)", ["Widget", 5])
print(db.last_insert_rowid)

for row in db.all("SELECT id, item, qty FROM orders"):
    print(row["item"], row["qty"])

db.begin()
try:
    db.run("INSERT INTO orders (item, qty) VALUES (?, ?)", ["Gadget", 10])
    db.commit()
except Exception:
    db.rollback()
    raise

print(db.backup_is_healthy, db.backup_queue_depth)
db.close()

Hydrate with Arkilian.hydrate_s3(db_path, endpoint, bucket, region, access_key, secret_key, prefix) before constructing the instance.

08 · SDKs

Go #

Import github.com/arkiliandb/arkilian/bindings/go/arkilian. The package compiles SQLite and Arkilian directly via cgo — no system libraries needed.

terminal
go get github.com/arkiliandb/arkilian/bindings/go/arkilian
main.go
db, err := arkilian.OpenDB("app.sqlite")
if err != nil {
  log.Fatal(err)
}
defer db.Close()

db.Exec("CREATE TABLE IF NOT EXISTS players (id INTEGER PRIMARY KEY, name TEXT, score INT)")

stmt, _ := db.Prepare("INSERT INTO players (name, score) VALUES (?, ?)")
defer stmt.Finalize()
stmt.BindText(1, "PlayerOne")
stmt.BindInt(2, 4200)
stmt.Step()

fmt.Println(db.Changes(), db.BackupQueueDepth(), db.BackupIsHealthy())

Use arkilian.Open(token, dbPath) when you manage auth tokens explicitly, and db.ResyncTriggers() after DDL issued through a raw handle.

09 · SDKs

Rust #

Add the arkilian crate. It exposes safe abstractions over arkilian-sys; prepare + use_stmt select the active statement for stepping and column reads.

main.rs
use arkilian::Database;

let db = Database::new("app.sqlite")?;
db.exec("CREATE TABLE IF NOT EXISTS scores (id INTEGER PRIMARY KEY, player TEXT, points INT)")?;

db.prepare("INSERT INTO scores (player, points) VALUES (?, ?)")?;
db.use_stmt(0)?;
db.bind_text(1, "Hero")?;
db.bind_int(2, 100)?;
db.step()?;
db.finalize()?;

println!("healthy: {}", db.backup_is_healthy());

Hydrate with arkilian::hydrate_s3(path, prefix, &s3config) before opening. Transactions use begin / commit / rollback.

10 · SDKs

PHP #

Requires PHP 7.4+ with FFI. Include bindings/php/Arkilian.php — reads return plain arrays, and every method returns $this for chaining where it makes sense.

app.php
<?php
require_once 'Arkilian.php';

$db = new Arkilian('app.sqlite');
$db->exec("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, text TEXT)");
$db->run("INSERT INTO messages (text) VALUES (?)", ["Hello from PHP"]);

print_r($db->all("SELECT id, text FROM messages"));

echo $db->backupIsHealthy() ? "healthy" : "degraded", "\n";
$db->close();

Prepared statements follow prepare → useStmt → bindText / bindInt → step → finalize. Hydrate statically with Arkilian::hydrateS3($path, $prefix, $s3Config).

11 · SDKs

C / C++ #

Link the static or shared library from CMake (ARKILIAN_BUILD_SHARED / ARKILIAN_BUILD_STATIC). Three independent connections eliminate cross-thread lock contention — never share one handle across threads.

main.c
#include "class.h"

arkilian *db = NULL;
db_init(&db, "app.sqlite");

db_exec(db, "CREATE TABLE items (id INTEGER PRIMARY KEY, title TEXT);");

db_prepare(db, "INSERT INTO items (title) VALUES (?);");
db_bind_text(db, 1, "Toolbox");
db_step(db);
db_finalize(db);

printf("queue=%d healthy=%d\n",
  db_backup_queue_depth(db), db_backup_is_healthy(db));

db_close(db);
terminal
gcc -I/usr/local/include/arkilian myapp.c -L/usr/local/lib -larkilian -lcurl -lpthread -lm -o myapp

12 · Operate

Telemetry #

Every binding exposes the same signals — Node.js uses getters (db.backupQueueDepth), Python properties (db.backup_queue_depth), Go methods (db.BackupQueueDepth()), and C functions (db_backup_queue_depth()).

SignalMeaning
backupQueueDepthRows in _pending_backup waiting for S3
backupOldestPendingAgeSecAge of the oldest pending row — replication lag
backupDeadLetterCountPoison rows parked in _dead_backup
backupThreadHeartbeatAgeMsMs since flush heartbeat (-1 when idle)
backupSnapshotHeartbeatAgeMsMs since snapshot heartbeat (-1 when idle)
backupChunkCountChunks shipped successfully, lifetime
backupLastChunkFlushAgeMsMs since the last successful upload
backupTriggerCoverage0 when every PK table has triggers; >0 otherwise
backupSkippedTableCountTables without a PRIMARY KEY, ignored by capture
backupHealthy / backupHealthFlags1 when core checks pass; bitmask below on failure

Health bitmask (ARK_HF_*)

BitFlag — set when…
0 · 0x001BACKUP_ENABLED — kill-switch off
1 · 0x002DEST_CONFIGURED — bucket + credentials present
2 · 0x004FLUSH_ALIVE — flush heartbeat fresh
3 · 0x008SNAPSHOT_ALIVE — snapshot heartbeat fresh
4 · 0x010QUEUE_BELOW_CAP — outbox under capacity
5 · 0x020SCHEMA_IN_SYNC — triggers match tables
6 · 0x040NO_DEAD_LETTER — DLQ empty
7 · 0x080MANIFEST_RESOLVED — manifest registry active
8 · 0x100NO_CAPTURE_GAP — no undrained drop gap

13 · Operate

Dead-letter queue #

Rows that fail upload ARKILIAN_MAX_ATTEMPTS times are parked in _dead_backup so one poison row never blocks the pipeline. Inspect and replay them with arkilian-dlq — it compiles against the bundled amalgamation with zero extra dependencies.

terminal
cc tools/arkilian-dlq.c src/deps/sqlite/sqlite3.c -Isrc/deps/sqlite -o arkilian-dlq

./arkilian-dlq app.sqlite --count     # how many poison rows
./arkilian-dlq app.sqlite --list      # payloads + error reasons
./arkilian-dlq app.sqlite --replay --dry-run
./arkilian-dlq app.sqlite --replay    # re-queue into _pending_backup

14 · Operate

Benchmarks #

Arkilian vs raw SQLite 3.46.1, same machine, schema, and connection settings (tests/bench_1m.c, Intel i7-9750H, journal_mode=WAL). Latency percentiles are identical — P50/P99 INSERT 256/512 µs, point SELECT 8/16 µs.

−2.2%

INSERT overhead vs raw SQLite

−0.2%

UPDATE overhead by primary key

29.4k

Batched ops/s — matches raw SQLite

8 µs

Point-select P50 latency, identical

Single-row op (100k)Raw SQLite → Arkilian
INSERT autocommit5,262 → 5,144 ops/s (−2.2%)
UPDATE by PK5,030 → 5,018 ops/s (−0.2%)
SELECT point by PK139,147 → 135,875 ops/s (−2.4%)
SELECT range 100 rows6,182 → 6,017 ops/s (−2.7%)
Batched INSERT (100k)Raw SQLite → Arkilian
Batch 1010,354 → 10,516 ops/s (+1.6%)
Batch 10022,975 → 23,231 ops/s (+1.1%)
Batch 1,00028,604 → 28,383 ops/s (−0.8%)
Batch 100,00029,304 → 29,432 ops/s (+0.4%)

Full curves, RSS telemetry, and reproduction steps live in BENCHMARK.md.

15 · Operate

Reference #

One surface, six spellings. Core verbs are identical everywhere: open, exec, run / prepare, read, transact, observe, close.

BindingOpen · write · read · close
Node.jsnew Arkilian(path) · exec/run · all<T> · transaction(fn) · close()
PythonArkilian(path) · exec/run · all · begin/commit/rollback · close()
GoOpenDB(path) · Exec/Prepare+Step · Begin/Commit/Rollback · Close()
RustDatabase::new(path) · exec/prepare+use_stmt+step · begin/commit/rollback
PHPnew Arkilian(path) · exec/run · all · begin/commit/rollback · close()
Cdb_init · db_exec/db_prepare+db_step · explicit SQL txn · db_close
BindingHydrate before open
Node.jsArkilian.hydrateS3(path, {endpoint, bucket, region, accessKey, secretKey, prefix})
PythonArkilian.hydrate_s3(path, endpoint, bucket, region, access_key, secret_key, prefix)
PHPArkilian::hydrateS3($path, $prefix, $s3Config)
Rustarkilian::hydrate_s3(path, prefix, &s3config)
Go / CConfigure env (.env) — hydration runs inside open/init

Ship your first snapshot today

Install the SDK, point it at SQLite, and let S3 hold the truth.