anything.solutions

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"]

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"])

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"},
)

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:

CallDoes
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.