Documentation
How TakoDB works
The full model in one page. Underneath is a real SQLite database — ACID, indexed, with window functions and JSON — reached over HTTPS with a token instead of a file path. Access is a token, authorization is SQLite's own, and bulk data moves as Parquet. Every endpoint is under /v1 and carries Authorization: Bearer <token>.
一 Data model
Each database is a single SQLite file in WAL mode, addressed by a validated slug matching [a-z0-9_]{1,48}. There is no shared schema and no cross-tenant table: isolation is a filesystem boundary rather than a row filter. TakoDB manages the files, tokens, and limits — the tables inside are entirely yours, created with ordinary CREATE TABLE.
Names are scoped to your account, not to the service as a whole. Your orders is yours alone; another account can keep its own orders with no collision, and the name is never a global fact you could probe for. The account is derived from the token, never taken from the request, so a token only ever reaches databases within the account that issued it — knowing another account's database name buys nothing.
Underneath is the full SQLite engine, not a stripped-down subset: ACID transactions, real indexes, window functions and CTEs, generated columns, STRICT typing, and JSON functions — over a database that comfortably grows into the gigabytes with sub-millisecond reads. A database per concern is exactly the workload SQLite is best at, so the model plays to the engine's strengths rather than working around them.
Cross-database reads
A request may ATTACH sibling databases the token also grants. Attachments are read-only and torn down at the end of the request, so a join across your own databases stays a single statement without granting standing access.
Your schema is yours
There are no datasets, manifests, or registered schemas. Tables are typically declared STRICT so each column keeps one type — but nothing forces that: keep a (id, body) table whose body is a JSON document and query it with SQLite's JSON functions, and the same database is a document store. Schema migrations are the caller's responsibility; the service never runs, versions, or infers changes to an existing table.
二 Access
Access is an opaque bearer token sent as Authorization: Bearer <token>. Only the token's hash is stored. Each token belongs to one account and pairs one capability — the maximum class of operation it may perform — with a grant: the specific set of databases within that account it applies to.
A token scoped to read on two databases can query and export from exactly those two and nothing else. Capabilities are deliberately narrow operation grants rather than roles, so a leaked ingest token can append rows and do nothing more. Every request is attributed to its token in an audit log.
The dashboard
You manage an account from the dashboard — create databases, mint tokens, and revoke them — without touching the API. Sign in at api.takodb.com/app. Sessions are cookie-based, sign-in is rate-limited per address and account, and a newly minted token is shown once at creation: copy it then, because only its hash is kept. Dashboard actions land in the same audit log as API requests.
四 API & data paths
Interactive queries return JSON; large datasets move as Parquet. Everything is under /v1.
Calling it from any language
There is no SDK, and there is nothing to install. The API is JSON over HTTPS with a bearer token, so the whole of a client is a few lines of whatever you already use. In Python that is the standard library:
import json import os import urllib.request BASE = "https://api.takodb.com/v1" TOKEN = os.environ["TAKODB_TOKEN"] def takodb(path, body=None, method="GET", content_type="application/json"): """Call TakoDB. body is a dict (JSON) or bytes (as-is).""" if isinstance(body, (bytes, type(None))): data = body else: data = json.dumps(body).encode() request = urllib.request.Request( BASE + path, data=data, method=method, headers={ "Authorization": f"Bearer {TOKEN}", "Content-Type": content_type, }, ) with urllib.request.urlopen(request) as response: payload = response.read() return json.loads(payload) if payload else None
Every Python example below uses that function and nothing else. An error arrives as an HTTPError whose body is the same JSON envelope the API always returns, so json.load(error)["error"]["code"] is the machine-readable reason:
try: takodb("/db/orders/sql", { "statements": [{"sql": "SELECT * FROM missing"}], }, method="POST") except urllib.error.HTTPError as error: problem = json.load(error)["error"] print(problem["code"], problem["message"]) # → sql_error no such table: missing
Query
The query endpoint is batch-first: a round trip costs far more than a local SQLite read, so an array of statements is the default shape. Parameters are always bound. atomic defaults to true and wraps the batch in one transaction.
curl
curl -X POST "https://api.takodb.com/v1/db/orders/sql" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "statements": [ { "sql": "SELECT id, total FROM sales WHERE region = ?", "params": ["eu"] } ], "format": "objects" }' # → {"results":[{"rows":[{"id":1,"total":42}, # {"id":2,"total":17}]}]}
Python
sql = "SELECT id, total FROM sales WHERE region = ?" result = takodb("/db/orders/sql", { "statements": [{"sql": sql, "params": ["eu"]}], "format": "objects", }, method="POST") rows = result["results"][0]["rows"] # → [{'id': 1, 'total': 42}, {'id': 2, 'total': 17}]
format is columns (default; column names sent once) or objects (larger, pipes into jq). Results are capped by row count and byte size; exceeding either returns 413 pointing at the export endpoint — never a silent truncation. There are no interactive transactions: read-then-write collapses into one statement with UPDATE … RETURNING or an upsert.
Attaching siblings
'{
"attach": {"cat": "catalog"},
"statements": [
{"sql": "SELECT s.id FROM sales s JOIN cat.items i ON i.id=s.item"}
]
}'
Idempotency
Mutating requests may carry Idempotency-Key: <opaque>. The response is stored against the key for 24 hours and replayed on retry, so a CI job killed after writing but before receiving its response can safely repeat. The key is scoped to the token, and reusing it with a different body is an error rather than a replay.
Bulk load
curl
BASE="https://api.takodb.com/v1/db/orders" curl -X POST "$BASE/load?table=sales&mode=append" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.apache.parquet" \ -H "Idempotency-Key: sales-2026-09-01" \ --data-binary @sales.parquet
Python
import io import pyarrow as pa import pyarrow.parquet as pq table = pa.table({ "id": [10, 11], "region": ["eu", "us"], "total": [5, 6], }) buffer = io.BytesIO() pq.write_table(table, buffer) takodb( "/db/orders/load?table=sales&mode=append", buffer.getvalue(), method="POST", content_type="application/vnd.apache.parquet", ) # → {'table': 'sales', 'mode': 'append', # 'format': 'parquet', 'rows_loaded': 2}
The body is Parquet, CSV, or NDJSON, inserted into an existing table in one transaction. mode is append, replace (truncate first), or upsert (needs a primary key). Passing create=true on a Parquet upload infers the schema and creates the table first, returning the exact DDL it ran; inference is Parquet-only and adds no constraints.
Bulk export
curl
BASE="https://api.takodb.com/v1" AUTH="Authorization: Bearer $TOKEN" JOB=$(curl -sX POST "$BASE/db/orders/export" -H "$AUTH" \ -d '{ "sql": "SELECT * FROM sales WHERE region = ?", "params": ["eu"], "format": "parquet" }' | jq -r .job_id) curl -s "$BASE/jobs/$JOB" -H "$AUTH" # once completed, stream the Parquet down curl -s "$BASE/jobs/$JOB/download" -H "$AUTH" \ -o sales.parquet
Python
import time import urllib.request job = takodb("/db/orders/export", { "sql": "SELECT * FROM sales WHERE region = ?", "params": ["eu"], }, method="POST") job_id = job["job_id"] while takodb(f"/jobs/{job_id}")["status"] == "queued": time.sleep(0.5) # the download is an authenticated GET, not a # signed public URL request = urllib.request.Request( f"{BASE}/jobs/{job_id}/download", headers={"Authorization": f"Bearer {TOKEN}"}, ) with urllib.request.urlopen(request) as response: with open("sales.parquet", "wb") as out: out.write(response.read())
Export runs the already-validated read query in SQLite on a read-only connection and streams the result to Parquet with pyarrow — the same engine that validated it, so no second SQL engine ever parses caller SQL. Identical export queries are cached exactly, keyed by a per-database write generation, so a stale result is unreachable rather than merely unlikely. Downloads stream from an authenticated endpoint rather than a public signed URL, so every access is attributable.
Key-value & documents
Not everything is a table. A typed key/value store lives at /v1/db/{db}/kv/{key} for the common "I just need to stash something" case — a cursor, a checkpoint, a last-run timestamp, a small blob of JSON — without writing SQL. Values are typed with the SQLite storage classes plus json; a missing key is a 404. GET needs read; PUT and DELETE need write.
curl
BASE="https://api.takodb.com/v1/db/app/kv" AUTH="Authorization: Bearer $TOKEN" # set — type is int, real, text, blob (base64), or json curl -X PUT "$BASE/cursor" -H "$AUTH" \ -d '{"type": "json", "value": {"page": 7, "done": false}}' # get curl "$BASE/cursor" -H "$AUTH" # → {"key":"cursor","type":"json", # "value":{"page":7,"done":false}}
Python
# set — type is int, real, text, blob (base64), or json takodb( "/db/app/kv/cursor", {"type": "json", "value": {"page": 7, "done": False}}, method="PUT", ) # get cursor = takodb("/db/app/kv/cursor") # → {'key': 'cursor', 'type': 'json', # 'value': {'page': 7, 'done': False}} takodb("/db/app/kv/cursor", method="DELETE")
For richer documents, keep JSON in an ordinary column and query it with SQLite's built-in JSON functions — the same database is then a document store, with SQL as the query language:
-- a schemaless "documents" table CREATE TABLE docs (id TEXT PRIMARY KEY, body TEXT); INSERT INTO docs VALUES ('u1', '{"name":"Ada","tags":["math","cs"]}'); -- query into the document SELECT body ->> '$.name' AS name FROM docs WHERE body ->> '$.tags[0]' = 'math';
Endpoints
| POST /v1/db/{db}/sql | run a batch of SQL statements |
| POST /v1/db/{db}/load | bulk-load Parquet / CSV / NDJSON |
| POST /v1/db/{db}/tables | create a table from a schema (ddl) |
| PUT/GET/DELETE /v1/db/{db}/kv/{key} | typed key-value entries |
| POST /v1/db/{db}/export | start a Parquet export job |
| GET /v1/jobs/{id} · /download | export job status and download |
| GET /v1/db · POST · PATCH · DELETE | list / create / promote / drop databases (admin) |
| POST /v1/tokens · DELETE | mint / revoke tokens (admin) |
| GET /v1/health · /health/live | health (admin) and public liveness |
Errors
Every error is a consistent envelope with a machine-readable code, a human message, and where relevant a statement index.
| Status | Meaning |
|---|---|
| 400 | Malformed request or SQL syntax error |
| 401 | Missing or invalid token |
| 403 | Token lacks the capability for this operation |
| 404 | No such database, key, or job |
| 409 | Write-lock contention after the busy timeout |
| 413 | Result or upload exceeds a cap — use export |
| 422 | Constraint violation |
| 429 | Rate limit or quota reached |
| 507 | Disk watermark reached; writes rejected |
/v1 is the only versioning mechanism: breaking changes get /v2, additive ones do not bump it.
五 Durability
Durability is declared per database, not imposed by the platform, so scratch data stays cheap while a source of record stays protected. The tier states whether replication is required and how long data is retained.
Live today: ephemeral only. Creating a derived or source database is refused while replication is off, rather than accepted and quietly left unprotected. The tiers below describe what each one means once replication is enabled, which will happen after a restore has been rehearsed end to end.
Ephemeral
- — No replication
- — Rebuildable at any time
- — For scratch & cache
Derived
- + Recomputable, costly to redo
- + Retained to avoid rework
- + Replication required
Source
- + Cannot be recomputed
- + Replication required
- + Retained the longest
Once enabled, replication ships the write-ahead log continuously; checkpointing is coordinated with it so frames are not recycled before they are shipped. The service's own state database is replicated like any other and is the first thing restored. The restore path is not left to chance: a scheduled drill rebuilds every durable database into a disposable directory and checks its integrity, so recovery is a routine that has already run — not a plan you first test during an incident.
六 Operation
TakoDB runs as a single ordered worker in front of WAL-mode SQLite: concurrent readers alongside one writer, so write ordering is a property of the design rather than a lock you manage. Expensive exports run in a separate worker pool and never block the request path.
- Concurrency
- One writer, many readers (WAL). Writes are ordered, not raced.
- Footprint
- Each database counts its file plus WAL and SHM sidecars against a quota.
- Limits
- Per-token rate limit, statement timeout, upload cap, and a disk watermark that rejects writes before the disk fills.
- Health
- An endpoint reports disk headroom, quota use, job counts, and integrity-check state.
Integrity checks use PRAGMA integrity_check and never a caller SQL path; a restore drill periodically rebuilds every database into a disposable directory to prove the backups actually restore.
Want to build?
Tell us what you're building and we'll get you set up.