Any HTTP client works — we use curl below. Rust developers can use the official SDK. Or point your existing Redis / Postgres / Kafka client straight at BteloDB.
Self-host with one command — data lives in ~/.btelo-db; set BTELO_S3_* to use your own S3 bucket in production.
Prefer zero-ops? Sign in and create a database in the cloud dashboard instead.
docker run -p 4382:4382 ghcr.io/btelolabs/btelo-db-release
Every request carries a bearer token. Locally, dev tokens of the form tnt_<tenant>_<tier> work out of the box. In the cloud, create an API key in the dashboard.
export BTELO=http://localhost:4382 export TOKEN=tnt_demo_pro
Tables auto-create on first write. A 200 means the data is fsync'd to the WAL (kill -9 won't lose it); offsets are strictly monotonic and immediately readable.
# create explicitly (optional)
curl -X PUT $BTELO/v1/tables/events -H "Authorization: Bearer $TOKEN" -d '{}'
# append a document — Idempotency-Key gives you free exactly-once
curl -X POST $BTELO/v1/tables/events/docs \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-d '{"user":"ada","level":"info","msg":"hello"}'Standard SELECT over your tables — aggregations, JSON paths (data['a']['b']), full-text (contains/token_match) and vector search (vector_distance).
curl -X POST $BTELO/v1/query -H "Authorization: Bearer $TOKEN" \
-d '{"sql":"SELECT level, COUNT(*) AS n FROM events GROUP BY level"}'A linearizable single-key store lives alongside your tables: get/put with CAS, atomic increment, TTL — and it's queryable from SQL via the _kv view.
curl -X PUT $BTELO/v1/kv/app/cfg -H "Authorization: Bearer $TOKEN" -d '{"value":{"theme":"dark"}}'
curl $BTELO/v1/kv/app/cfg -H "Authorization: Bearer $TOKEN"
curl -X POST $BTELO/v1/kv/app/hits/increment -H "Authorization: Bearer $TOKEN" -d '{"delta":1}'Tail a table as an event stream. Reconnect with Last-Event-ID for exactly-resumed delivery — no gaps, no dupes.
curl -N "$BTELO/v1/tables/events/tail?from_offset=latest" -H "Authorization: Bearer $TOKEN"
The official client wraps all of the above with automatic retries, free idempotency keys, pagination and SSE reconnect.
cargo add btelo-db-client tokio serde_json
use btelo_db_client::BteloClient;
#[tokio::main]
async fn main() -> Result<(), btelo_db_client::BteloError> {
let db = BteloClient::new("http://localhost:4382", "tnt_demo_pro");
db.create_table("events", None).await?;
db.append("events", &serde_json::json!({ "user": "ada", "msg": "hello" })).await?;
let res = db.query("SELECT COUNT(*) AS n FROM events", &[]).await?;
println!("{:?}", res.rows);
Ok(())
}BteloDB speaks the Postgres, Redis and Kafka wire protocols, so any language's mature, connection-pooled native driver talks to it unchanged — there's no BteloDB SDK to install. Pick the protocol that fits and point your client at the port (publish it when self-hosting). Password / AUTH = your API token.
redis-cli -p 4380 -a tnt_demo_pro # Redis wire · KV / INCR / TTL psql postgres://btelo:tnt_demo_pro@localhost:4383/btelo # Postgres wire · SQL # Kafka clients: bootstrap localhost:4384 · SASL/PLAIN (topic = table)
SELECT and INSERT map straight to query and append; UPDATE/DELETE map to async batch mutations. Use psql, JDBC, pgx, node-postgres, psycopg, sqlx — anything that speaks pgwire.
# Postgres wire · port 4383 · password = your API token
psql "postgres://btelo:tnt_demo_pro@localhost:4383/btelo"
# Python · psycopg
cx = psycopg.connect("postgres://btelo:tnt_demo_pro@localhost:4383/btelo")
cx.execute("INSERT INTO events (msg) VALUES (%s)", ["hello"])
print(cx.execute("SELECT count(*) FROM events").fetchone())
# Node · pg
const cx = new Client("postgres://btelo:tnt_demo_pro@localhost:4383/btelo");
await cx.connect();
console.log((await cx.query("SELECT count(*) FROM events")).rows);
# Go · pgx
conn, _ := pgx.Connect(ctx, "postgres://btelo:tnt_demo_pro@localhost:4383/btelo")
# Java · JDBC
DriverManager.getConnection("jdbc:postgresql://localhost:4383/btelo", "btelo", "tnt_demo_pro");RESP2: GET/SET/DEL/EXISTS/MGET, atomic INCR, SCAN and TTL — backed by the same linearizable KV (also queryable from SQL via the _kv view). Use redis-py, ioredis, go-redis, jedis, or redis-cli.
# Redis wire · port 4380 · AUTH = your API token
redis-cli -p 4380 -a tnt_demo_pro
# > SET app:greeting "hello" | INCR app:hits | EXPIRE app:greeting 60
# Python · redis-py
r = redis.Redis(port=4380, password="tnt_demo_pro")
r.set("app:greeting", "hello"); print(r.incr("app:hits"))
# Node · ioredis
const r = new Redis({ port: 4380, password: "tnt_demo_pro" });
await r.set("app:greeting", "hello"); await r.incr("app:hits");
# Go · go-redis
rdb := redis.NewClient(&redis.Options{Addr: "localhost:4380", Password: "tnt_demo_pro"})
rdb.Set(ctx, "app:greeting", "hello", 0)SASL/PLAIN auth; topic = table. Produce maps to append and Fetch to pull, so events produced here are immediately queryable with SQL. Use confluent-kafka, the Java client, franz-go, or rdkafka.
# Kafka wire · port 4384 · SASL/PLAIN, password = your API token (topic = table)
bootstrap.servers=localhost:4384
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.username=btelo
sasl.password=tnt_demo_pro
# Python · confluent-kafka
p = Producer({"bootstrap.servers": "localhost:4384",
"security.protocol": "SASL_PLAINTEXT", "sasl.mechanism": "PLAIN",
"sasl.username": "btelo", "sasl.password": "tnt_demo_pro"})
p.produce("events", value=b'{"msg":"hello"}'); p.flush()
# Java · kafka-clients
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule \
required username="btelo" password="tnt_demo_pro";