TiniDB
TiniDB is a private SQLite database for each of your applications. Every
database runs on three nodes with a Raft quorum, so a write is committed
before the request returns and a single node failure does not take the
database offline. Query it from the console, over HTTP, or from a Worker
through env.DB.
Databases are independent of Workers and buckets. You can create one, query it, and connect an application without deploying a Worker. Billing is metered by rows read, rows written, and stored bytes.
Create a database
Open Resources → TiniDB and choose Create database. Give it a name; the console generates a unique slug and a write token. Placement starts immediately and the database becomes ready once its three replicas are healthy.
The list shows each database's status and creation date. Select a database to open its page.
API tokens
Open the Access tab to manage tokens. A token is scoped to one database and is either read only or read and write. The full secret is shown once, when it is created; after that the console only shows its prefix.
- Create token opens a dialog for the token name and access level.
- Select the eye icon on a row to see the token's scope, prefix, creation time, and last use.
- Revoke stops every client using that token immediately.
Use a separate token per application so one can be rotated or revoked without affecting the others. A Worker binding creates its own token, and that token cannot be revoked on its own: unbind the database from the Worker to remove it.
Query tab
The Query tab is a SQL editor with a schema sidebar. Reads run against the leader; writes return after a quorum commits them. Run one statement at a time with Ctrl / ⌘ + Enter. Results are capped at 10,000 rows and render in the results table below the editor.
Select a table, view, index, or trigger in the sidebar to insert its name at the cursor.
HTTP API
Every database has a query endpoint. Send a JSON body with a
sql string and an optional params array. Authenticate
with a token from the Access tab.
import requests
response = requests.post(
"https://anything-advertising.com/db/v1/<slug>/query",
headers={"Authorization": "Bearer <token>"},
json={
"sql": "SELECT * FROM users WHERE id = ?",
"params": [42],
},
)
rows = response.json()["results"]
const response = await fetch(
"https://anything-advertising.com/db/v1/<slug>/query",
{
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
sql: "SELECT * FROM users WHERE id = ?",
params: [42],
}),
},
);
const { results } = await response.json();
A successful response includes the rows and execution metadata:
{
"ok": true,
"success": true,
"results": [{"id": 42, "name": "Ada"}],
"meta": {
"duration": 0.004,
"rows_read": 1,
"rows_written": 0,
"rows_returned": 1,
"changes": 0
}
}
Use params instead of string interpolation. A read-only
token, or any token used for a write, is rejected with a JSON error.
Standard SQL clients
The same database also speaks the libSQL HTTP protocol, so you can use
@libsql/client in JavaScript or libsql-client in
Python instead of the JSON API. Keep the trailing slash after the slug. The
token is the same one from the Access tab.
from libsql_client import create_client_sync
db = create_client_sync(
url="https://anything-advertising.com/db/v1/<slug>/",
auth_token="<token>",
)
result = db.execute(
"SELECT id, name FROM users WHERE id = ?",
[42],
)
for row in result.rows:
print(row["id"], row["name"])
import { createClient } from "@libsql/client/web";
const db = createClient({
url: "https://anything-advertising.com/db/v1/<slug>/",
authToken: "<token>",
});
const result = await db.execute({
sql: "SELECT id, name FROM users WHERE id = ?",
args: [42],
});
execute and batch work as usual in both
clients; JavaScript also has db.executeMultiple. A batch runs
as one atomic transaction, and blobs round-trip as binary values.
Interactive transactions are not available over HTTP because rqlite has
no session that survives a request. Use db.batch() when
several statements must commit together. PRAGMA statements
are blocked, a single response carries at most 10,000 rows, and a batch
accepts up to 100 statements.
Custom hostname
You can serve the API from a domain or subdomain you own. Follow the Custom domains guide and pick the database as the target. The custom hostname serves the same API paths:
import requests
response = requests.post(
"https://db.example.com/query",
headers={"Authorization": "Bearer <token>"},
json={"sql": "SELECT 1"},
)
const response = await fetch("https://db.example.com/query", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({ sql: "SELECT 1" }),
});
The database token is still required. Removing the host stops serving the API at that address; the database and its tokens stay in place.
Bind to a Worker
Open a Worker, choose Settings, and add the database
under TiniDB. Give the binding an alias such as
DB; Worker code then uses env.DB. Binding or
unbinding restarts the Worker so the new alias appears.
export default {
async fetch(request, env) {
const row = await env.DB
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' LIMIT 1")
.first();
const result = await env.DB
.prepare("INSERT INTO visits (day) VALUES (?)")
.bind(new Date().toISOString().slice(0, 10))
.run();
return Response.json({ table: row?.name, inserted: result.meta.changes });
},
};
The binding implements the familiar prepared-statement API:
| Call | Does |
|---|---|
env.DB.prepare(sql) | Builds a statement. Chain .bind(...params) to add positional or named parameters. |
.all() | Runs the statement and returns { results, success, meta }. |
.first(column) | Returns the first row, or one column from it. Returns null when there are no rows. |
.run() | Runs a write and returns { success, meta }. |
.raw() | Returns rows as arrays of values instead of objects. |
env.DB.batch(statements) | Runs prepared statements in one request and returns their results in order. |
env.DB.exec(sql) | Runs one or more SQL statements without parameters. |
Metrics
The Metrics tab shows the last 24 hours of queries, rows read, rows written, and query latency. The Overview tab shows the same totals as a summary. Monthly usage and estimated cost are on the Settings tab.
Settings
The Settings tab holds the connection details, Worker bindings, monthly
usage, and the delete action. The query endpoint and a copyable
curl example are in the Connection card.
Deleting a database removes its replicas, tokens, and bindings, and cannot
be undone; the console asks for confirmation first.
Backups are written automatically within six hours of creation and appear on the Overview tab. They are verified snapshots on the database nodes.
Limits and billing
Each account has limits for the number of databases, total storage, monthly rows read, and monthly rows written. Usage and limits are shown in Settings; request a higher limit from the TiniDB section there. Limits do not change the database's data or its connection details.
TiniDB is billed by rows read, rows written, and stored bytes. Storage is metered continuously; row counts reset on the first of the month. See Billing & metrics for how usage is charged.