rindle serve (and rindle up, which runs it) serves everything on one port: HTTP requests,
the live-query stream, and WebSocket upgrades. rindle up uses port 7600, or the next free port
up to 7699, and writes the address to .env as RINDLE_URL.
The TypeScript clients wrap all of this. You need this page only to call Rindle from another language or to debug.
Auth
Send the token as a bearer:
Authorization: Bearer <token>- Locally, there is no token unless you start with
rindle up --token <token>. A server bound to127.0.0.1with no token accepts every request. - On any other address, a token is required. Without one,
/v1/sql/*answers403. - A token in
RINDLE_URL(https://:token@db.example.com) is a client convention. The clients send it as the bearer; the server only reads the header.
A wrong or missing token is 401.
Responses
Success bodies are JSON. Write responses carry a commit_cursor, the position of that write in
the log, and a routing object:
{
"result": { … },
"commit_cursor": "w:00000000000000a3",
"routing": { "served_by": "standalone", "applied_lag_ms": null, "fence_fallback": false }
}Errors under /v1/sql/ have a code:
{ "code": "STATEMENT_INVALID", "message": "write request requires a non-empty idempotency_key", "retry_scope": "never" }| Status | Meaning |
|---|---|
400 |
Bad request, or SQL that doesn’t compile. code says which. |
401 / 403 |
Wrong token, or no token on a public address. |
409 |
A conflict, such as MIGRATION_CHECKSUM_MISMATCH. |
413 |
Body over 80 MiB, or a result over the row limit (RESULT_LIMIT). |
503 |
Busy or starting up. Retry after the Retry-After header. |
Every response has a Rindle-Boot-Id header that changes when the server restarts.
SQL
All POST, all JSON.
Statements
A statement is one SQL statement and its arguments:
{ "sql": "INSERT INTO issue (title, closed) VALUES (?, ?)", "args": ["first", 0] }argsis an array for?placeholders, or an object for named ones (:title).- Values are JSON strings, numbers, booleans and
null. Send 64-bit integers that don’t fit in a double as{"$rindle":"i64","value":"9007199254740993"}. want_rows: falseskips returning rows.
A statement result:
{
"columns": [{ "name": "id", "decltype": "INTEGER" }],
"rows": [[{ "$rindle": "i64", "value": "1" }]],
"rows_affected": 0,
"last_insert_rowid": null,
"rows_read": null,
"rows_written": null
}rows are arrays in column order. Integers come back as {"$rindle":"i64","value":"…"}, so
none lose precision. last_insert_rowid is a string.
Idempotency keys
Every write needs an idempotency_key. Sending the same request with the same key again returns
the first result instead of writing twice, so retries are safe. The format is sql1. + the
current time in milliseconds (13 digits) + . + 32 random lowercase hex digits:
sql1.1760029200000.9f86d081884c7d659a2feaa0c55ad015 /v1/sql/execute
Runs one statement.
{ "statement": { "sql": "SELECT * FROM issue WHERE id = ?", "args": [1] } }Returns { "result": … }. Reads need no key. Writes need idempotency_key. Schema changes go
through /v1/sql/migrate.
Reads take an optional consistency: session (default), strong, or eventual.
/v1/sql/batch
Runs statements in one transaction. All commit or none do.
{
"statements": [
{ "sql": "INSERT INTO comment (issue_id, body) VALUES (?, ?)", "args": [1, "a"] },
{ "sql": "INSERT INTO comment (issue_id, body) VALUES (?, ?)", "args": [1, "b"] }
],
"idempotency_key": "sql1.1760029200000.9f86d081884c7d659a2feaa0c55ad015"
}Returns { "results": [ … ] }, one per statement.
/v1/sql/execute-multiple
Runs a script of ;-separated statements, each committed on its own. No arguments.
{ "sql": "DELETE FROM comment; DELETE FROM issue;", "idempotency_key": "sql1.…" }Returns { "commit_cursor": … }.
/v1/sql/migrate
Applies a schema change once, by name. Sending the same id again is a no-op.
{ "id": "0001_init", "sql": "CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT);" }Returns { "applied": true, "commit_cursor": … }, or "applied": false if it was already
applied. Sending a different script under an applied id is 409 MIGRATION_CHECKSUM_MISMATCH.
/v1/sql/transactions
Interactive transactions, for when a statement depends on an earlier one’s result.
| Route | Body | Returns |
|---|---|---|
/v1/sql/transactions |
{ "read_only"?: bool } |
{ "transaction_id": … } |
/v1/sql/transactions/{id}/execute |
{ "operation_id": "1", "statements": [ … ] } |
{ "results": [ … ] } |
/v1/sql/transactions/{id}/commit |
{ "operation_id": "2" } |
{ "commit_cursor": … } |
/v1/sql/transactions/{id}/rollback |
{ "operation_id": "2" } |
operation_id counts up from 1 within the transaction; a repeated id is a retry.
Live queries
A client is one browser tab (or other consumer). It has one stream and any number of queries. Your API server makes these calls on the client’s behalf; see Architecture.
Each call names the client in headers:
| Header | |
|---|---|
Rindle-Client |
Required. The client’s id. |
Rindle-Query |
The query’s id, a number. |
Rindle-User |
Optional. Once set, calls for this client with a different user are refused. |
| Method and route | Body | Returns |
|---|---|---|
GET /v1/sql/client/stream |
The stream, below. | |
POST /v1/sql/client/queries |
{ "sql": "…", "params": [ … ] } |
200. Rows arrive on the stream. 400 {"error": …} with the compiler’s message if the SQL can’t be kept live. |
DELETE /v1/sql/client/queries/{id} |
204 |
|
DELETE /v1/sql/client |
{ "ended": n }. Ends every stream for Rindle-Client, or for Rindle-User if no client is given. |
A client may hold 256 queries, and a user 32 clients. A client whose stream drops is kept for 5 seconds, so a reconnect resumes its queries.
The stream
GET /v1/sql/client/stream returns server-sent events.
The first event lists the client’s queries. Each later event carries an id and one JSON frame:
data: {"t":"client","server":"b-3f2a…","queries":[1,2]}
id: 1
data: {"t":"hello","queryId":1,…}
id: 2
data: {"t":"batch","queryId":1,"batch":{"events":[…]}}| Frame | |
|---|---|
hello |
A query started. Carries its schema. |
batch |
Row changes for a query: adds, removes and edits. |
progress |
Every query is current as of this point. |
queryError |
A query failed. |
A : keepalive comment arrives every 15 seconds.
Health
| Route | Auth | Returns |
|---|---|---|
GET /readyz |
none | 200, or 503 {"ready": false, "reason": …} while starting |
GET /version |
none | {"service": "rindled", "sha": …} |
Internal routes
The server also has routes the clients use internally (/materialize, /schema,
/execute-sql-txn, the WebSocket protocol, and others). They can change between releases; use
the routes on this page.