蛸TakoDB by vimerk ↗

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.

one file per database
orders.sqlite + ‑wal + ‑shm events.sqlite + ‑wal + ‑shm

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.

read · query & export write · rows ddl · schema admin · manage

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.

三 Authorization

Rather than guess a statement's intent by parsing SQL text — a losing game against every syntax quirk — every statement runs under SQLite's own authorizer callback, which vetoes any action the capability does not permit. The engine that executes the query is the one that polices it, so validation and execution cannot diverge.

Requesttoken + SQL → Sessioncapability + grant → Authorizerper statement → 〇 allow   × deny

Each request is scoped to one authorized session that owns the connection, the capability check, the transaction boundary, and any read-only attachments — a single place where isolation is enforced.

四 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:

A complete TakoDB client
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:

Errors are ordinary HTTP
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.

POST /v1/db/{database}/sql

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

Read-only attachment within a request
'{
"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

POST /v1/db/{database}/load?table=…&mode=…

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

POST /v1/db/{database}/export → job; GET /v1/jobs/{id}

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.

PUT / GET / DELETE /v1/db/{database}/kv/{key}

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:

JSON documents, queried with SQL
-- 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}/sqlrun a batch of SQL statements
POST /v1/db/{db}/loadbulk-load Parquet / CSV / NDJSON
POST /v1/db/{db}/tablescreate a table from a schema (ddl)
PUT/GET/DELETE /v1/db/{db}/kv/{key}typed key-value entries
POST /v1/db/{db}/exportstart a Parquet export job
GET /v1/jobs/{id} · /downloadexport job status and download
GET /v1/db · POST · PATCH · DELETElist / create / promote / drop databases (admin)
POST /v1/tokens · DELETEmint / revoke tokens (admin)
GET /v1/health · /health/livehealth (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.

StatusMeaning
400Malformed request or SQL syntax error
401Missing or invalid token
403Token lacks the capability for this operation
404No such database, key, or job
409Write-lock contention after the busy timeout
413Result or upload exceeds a cap — use export
422Constraint violation
429Rate limit or quota reached
507Disk 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.

Tier

Ephemeral

  • — No replication
  • — Rebuildable at any time
  • — For scratch & cache
Tier

Derived

  • + Recomputable, costly to redo
  • + Retained to avoid rework
  • + Replication required
Tier

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.

Contact us →