Skip to content
Contents

HTTP API

The routes rindle serve accepts

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 to 127.0.0.1 with no token accepts every request.
  • On any other address, a token is required. Without one, /v1/sql/* answers 403.
  • 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] }
  • args is 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: false skips 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.