Skip to content
Reference contents

ReferenceConfiguration & limits

Run the daemon (rindled)

Configure rindled as a standalone database authority or a read follower, and understand its network and recovery requirements.

View as Markdown

rindled is the long-running server that maintains live queries and streams changes to subscribers. Application code usually starts it through rindle dev. This page is for operators who configure or supervise the process directly.

The daemon has two operating modes:

  • Standalone authority — one source-less daemon owns a wal2 SQLite file and serves reads, writes, migrations, SQL, materializations, and subscriptions at one origin.
  • Fleet follower — the daemon holds a read-only wal2 replica, derives deltas from a rindle-replicator master’s ordered stream, and serves reads and subscriptions. Every write endpoint — and the whole public /v1/sql surface, reads included — remains fenced with a fail-closed 409 that names the master.

The browser subscribes to the daemon’s public WebSocket. Your API server authorizes named operations and drives its private HTTP control plane. In standalone, its read and write transports point at the same origin; in a fleet, a split client sends writes to the master and reads to a follower.

See the architecture for how the three tiers fit and deploying & scaling for the durability and scale tradeoffs of each posture. This page covers the daemon itself.

The binary

rindled lives in the rindle-server crate and ships as a prebuilt, per-platform binary with @rindle/cli. For local dev you rarely invoke it directly — rindle dev renders your rindle.ncl, supervises the selected profile, and runs your application with topology-derived bindings. Use rindle up when you deliberately want only the data tier. It’s the first thing the synced-app quickstart does. This page is the daemon in full — the config, the two planes, restart recovery, and the engine underneath — for when you run it yourself.

To run a daemon directly (in production, or under your own supervisor), point a release, container, or otherwise supervised rindled binary at a JSON config:

rindled --config follower.json
# Or build from a source checkout (the manifest declares Apache-2.0):
cd rust
cargo build -p rindle-server --bin rindled --profile release-server
./target/release-server/rindled --config follower.json

Source builds use release-server so the daemon can contain a panic from an individual mutation. The ordinary release profile aborts the process on panic.

The config declares its file, ports, auth, worker count, and posture. A follower has one change source — the write-master’s fan-out stream:

{
  "db": "follower.db",
  "httpPort": 7600,
  "wsPort": 7601,
  "authToken": "dev-daemon-token",
  "nWorkers": 4,
  "sources": [
    { "kind": "replicator", "name": "rindle-master", "url": "ws://127.0.0.1:7610/subscribe" }
  ]
}

A standalone config is deliberately source-less and opts in explicitly. It needs a second, distinct bearer for the public SQL surface when exposed beyond loopback:

{
  "db": "standalone.db",
  "httpPort": 7600,
  "wsPort": 7601,
  "bindHost": "127.0.0.1",
  "authToken": "private-control-token",
  "sqlAuthToken": "server-sql-token",
  "nWorkers": 4,
  "standalone": true,
  "sources": [],
  "tables": []
}

Create application tables through migrations. The tables field is primarily useful to bootstrap a directly spawned daemon in a test or embedding.

  • db — the file-backed wal2 SQLite database (defaults to rindle.db). It is the authority in standalone and a replica in follower posture.
  • httpPort / wsPort — the control and subscription ports. 0 binds an ephemeral port (handy in tests — the chosen port comes back in the readiness signal).
  • authToken — the private control-plane bearer. A non-loopback bind refuses to start without it.
  • sqlAuthToken — the public /v1/sql/* bearer. Standalone requires this surface for @rindle/sql-client; it must differ from authToken when both are present.
  • nWorkers — IVM worker threads in the underlying Cluster. If omitted, the binary uses host parallelism, with a minimum of 2 and a fallback of 2.
  • defaultLeaseTtlMs — how long a materialization lease lives without renewal.
  • queryFamilies — group subscriptions that differ only in a root-level equality literal into one shared pipeline (a parameterized query family). On by default; false is the kill switch. Subscribers see the same frames either way.
  • familyShardBindings — how many partitions one family pipeline carries before the daemon opens another (on another worker) for new members. Default 2048.
  • joinPrecheckPerJoin / joinPrecheckPerGraph — the memory bounds of the join membership pre-check (a hash probe that skips the parent lookup a child-table write would otherwise cost every nested-relationship query): distinct parent keys one join may track, and the total across a worker’s joins. On by default at 4096 / 65536; joinPrecheckPerJoin: 0 turns it off. Subscribers see the same frames either way.
  • standalone / sources — choose exactly one posture. standalone: true requires zero sources. Follower posture requires exactly one kind:"replicator" source. Omitting the standalone flag never guesses from an empty list; it stays a follower and fails closed. A follower receives schema DDL from its master, while a standalone daemon applies it locally through /migrate.

On a successful start rindled prints exactly one line of JSON to stdout, so a supervisor or test runner can wait on it:

{"ready":true,"httpPort":7600,"wsPort":7601}

Two listeners and trust surfaces

The daemon exposes two network listeners, kept separate so the untrusted browser plane and trusted server-to-server traffic never share a door:

Plane Port Who connects Carries
Public WebSocket wsPort browser clients the normalized protocol — init, subscribe / unsubscribe, and cv-stamped snapshot + delta frames out
Private HTTP control httpPort your API server / operator all postures: /materialize, /execute-sql-read, /dematerialize, /schema, /version; standalone also: /execute-sql-txn, /mutate-session/*, /reject-mutation, /migrate
Public SQL over HTTP httpPort trusted SQL clients / API server /v1/sql/*, protected independently by sqlAuthToken; standalone owns reads and writes here — a follower refuses the entire surface

In follower posture, the write endpoints live on the rindle-replicator master. If you point a write at the follower, it refuses it with a fail-closed error that names the master. A misrouted write cannot silently vanish. The same 409 fences the entire public /v1/sql surface, reads included: session-consistency cursors are minted in the master’s journal sequence, which a follower’s local commit counter cannot fence, so honoring them would return silently stale rows or spurious CURSOR_HISTORY_LOST. Send all /v1/sql traffic to the write-master — in a fleet, the unified edge does this for you. Standalone enables its mutation, migration, session, and public SQL write surfaces, but not fleet change-source ingress such as /apply-row-change-txn.

The control routes require authToken when one is set; /v1/sql/* requires the separate sqlAuthToken. These are operator-level topology details. A fleet edge can expose one RINDLE_URL and one server-only RINDLE_DATABASE_TOKEN, routing each call internally. A loopback standalone rindle dev connection also exports one application origin; a directly networked standalone API server configures its private daemon client and public SQL/database transport with their respective bearers. Browsers never speak either trusted surface directly. The low-level @rindle/daemon-client package remains the typed client for custom self-hosted routing and supervisor integrations.

Standalone durability contract

Standalone deliberately has no HCTree journal, follower fan-out, rindle-backup plane, PITR, or automatic failover. Desktop/local software owns an ordinary SQLite snapshot (VACUUM INTO, the backup API, or an OS backup) and its RPO. A hosted micro deployment keeps one persistent volume and restores a completed provider volume snapshot into a replacement instance; recovery has downtime and may lose writes back to the advertised snapshot interval.

A product-initiated snapshot before a destructive migration or risky upgrade must stop intake, drain or roll back open sessions, and quiesce/close the writer first. Provider-scheduled snapshots should be advertised only after a restore drill has captured an active wal2 database, restored it to a new volume, passed SQLite integrity checks, and re-proved view-after-write equals a fresh query.

A standalone wal2 file cannot be promoted in place to an HCTree master. Moving to a fleet is an export/import into a new store followed by a routing cutover. Choose the fleet profile from the start when continuous journal recovery, followers, or read fan-out are requirements.

Restart recovery: the boot id

rindled keeps no durable materialization state — on restart it has the data (it’s file-backed) but no live queries or pins. So it stamps every control-plane response with a boot id header that changes when it restarts. The HttpRindleDaemonClient surfaces it via onBootId, and your API server re-asserts its pinned queries when it fires:

const daemonToken = process.env.RINDLE_DAEMON_TOKEN ?? process.env.RINDLE_DATABASE_TOKEN!;
const daemon = new HttpRindleDaemonClient({
  baseUrl: process.env.RINDLE_URL!,
  headers: { authorization: `Bearer ${daemonToken}` },
  onBootId: () => api.assertPins().catch(console.error), // re-warm after a restart
});

The hook rides responses you already make, so there’s no polling — the next control-plane call after a restart re-establishes the warm set.

Under the hood: the Cluster

rindled runs the multi-threaded Cluster engine from rindle-replica. Where the single-thread Db advances every query on one thread, Cluster shards queries across a pool of IVM worker threads behind a single writer/coordinator. In standalone, writes serialize on that wal2 writer, while each read-only public SQL transaction runs beside it on its own wal2 reader snapshot — see Rindle SQL. On a follower, pre-formed transactions arrive over the master’s stream. The same per-transaction handshake keeps IVM derivation correct:

  1. The coordinator opens its controlled write transaction. Standalone captures SQL row changes; a follower receives captured changes from its source.
  2. Workers pin read-only snapshots of the pre-commit database. Captured changes stream through a batch overlay as each worker derives its query deltas.
  3. Workers can send provisional Changed slices before the writer commits. A transaction can have several slices for one query.
  4. After a successful commit, each worker sends Progressed after its changes. The drain computes connection progress, and clients release staged data through that confirmed point.

A query lives on one worker. Its changes retain order, but receiving a data slice alone does not establish a committed result. A query fault is terminal: the consumer discards its provisional data and obtains a fresh subscription. An abort after streaming can also cause this recovery.

The derivation connections do not replay writes or roll back a second copy of the transaction. SQLite supplies the stable snapshot, and the overlay supplies the changing rows. See the embedded runtime for the raw Cluster delivery contract.

The query planner

The daemon runs the cost-based join-flip planner — it annotates each flippable EXISTS with a flip decision before lowering, and picks the cheaper drive side from a real-SQLite cost model. It is result-preserving (only the work changes, never the rows) and is on by default (Cluster::open enables it). Opting out is in-process only today, via Cluster::open_with_planning(path, n, false), not yet through the config file.

Scope

rindled is the productionizing read server, but it is young. The replica’s schema constraints apply — plain tables without triggers or generated columns, and adapter-specific numeric restrictions. See replica and views. The database bearer is server-wide. Finer-grained authz lives in your API tier.

This page runs one daemon. Standalone is intentionally one authority and one file; starting a second writable copy would create two authorities, not a replica. To add continuous journal recovery or scale reads across affinity-placed followers, move to the fleet profile described in deploying & scaling. That move is an export/import into a new HCTree store followed by a routing cutover, not an in-place promotion of the standalone wal2 file.

Next steps