# Run the daemon (rindled)

rindled — the always-on server you run like Postgres: the config, the two network planes, boot-id restart recovery, and the multi-threaded Cluster engine underneath.

**`rindled`** is the read-serving tier of the [Rindle data tier](/docs/deploy) — the
always-up server you run like Postgres. It is a **read-follower**: it holds a SQLite
replica of the data and the live IVM pipelines, derives the incremental delta after
every write it receives from the [`rindle-replicator`](/docs/deploy) write-master, and
streams normalized, cv-stamped updates to every subscriber. Writes never enter a
follower — they go to the write-master; a follower has **no write ingress at all**. The
browser subscribes to a follower's public WebSocket; your
[API server](/docs/api-server) reads and materializes over its private HTTP control
plane (and sends writes to the write-master).

See [the architecture](/docs/architecture) for how the three tiers fit and
[deploying & scaling](/docs/deploy) for the write-master that feeds it; this page is
about running the follower daemon itself.

## The binary

`rindled` lives in the `rindle-server` crate and ships as a prebuilt, per-platform
binary with [`@rindle/cli`](/docs/rindle-cli). For local dev you rarely invoke it
directly — `rindle up` renders your [`rindle.ncl`](/docs/rindle-cli) and supervises
the whole pair (the `rindle-replicator` write-master **and** its follower) for you.
It's the first thing the [synced-app quickstart](/docs/synced-app-quickstart) does.
This page is the follower daemon in full — the config, the two planes, restart
recovery, and the engine underneath — for when you run it yourself.

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

```bash
rindled --config follower.json
# …or, with a Rindle source license (rindle-server is a commercial crate):
cargo build -p rindle-server --bin rindled --release
./target/release/rindled --config follower.json
```

The config declares the follower's replica file, the two ports, an optional auth
token, the worker count, and the **change source** it tails — the write-master's
fan-out stream:

```json
{
  "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" }
  ]
}
```

- **`db`** — the follower's file-backed wal2 SQLite **replica** (defaults to
  `rindle.db`); the write-master owns the authoritative copy.
- **`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`** — a bearer token required on both planes. Omit it for open local
  dev.
- **`nWorkers`** — IVM worker threads in the underlying `Cluster` (defaults to 2).
- **`defaultLeaseTtlMs`** — how long a materialization lease lives without renewal.
- **`sources`** — exactly one `kind:"replicator"` change source: the write-master's
  fan-out WebSocket (`ws://…:7610/subscribe`). The follower dials out, tails the
  totally-ordered change log, and applies it. The **schema** arrives the same way —
  DDL replicates from the master — so there's no table list here: new tables are
  auto-discovered as the master's [migrations](/docs/schema) flow through, and
  `rindle schema gen` regenerates the client schema from the follower's `/schema`.

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

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

## Two planes

A follower exposes two network surfaces, kept separate so the untrusted browser plane
and the trusted server-to-server plane 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 | **reads only** — `/materialize` (mint a query lease), `/execute-sql-read` (a raw read), `/dematerialize`, `/schema`, `/version` |

The **write** endpoints — `/execute-sql-txn`, `/migrate`, `/mutate-session/*`,
`/reject-mutation`, `/apply-row-change-txn` — live on the
[`rindle-replicator`](/docs/deploy) write-master, **not** on a follower. Point writes
at a follower and it refuses them with a fail-closed error that names the master, so a
misrouted write can't silently vanish.

Both planes require the bearer `authToken` when one is set. Clients never speak the
control plane directly — they go through your [API tier](/docs/api-server), which holds
the query registry and the authoritative mutators and routes reads to the follower and
writes to the write-master (its [`SplitDaemonClient`](/docs/api-server#talking-to-the-daemon)
does the split). The [`@rindle/daemon-client`](/docs/api-server#talking-to-the-daemon)
package is the typed client for the HTTP plane.

## 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:

```ts
const daemon = new HttpRindleDaemonClient({
  baseUrl: "http://127.0.0.1:7600",
  headers: { authorization: `Bearer ${token}` },
  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`](/docs/replica-and-views) advances every query on one
thread, `Cluster` shards queries across a pool of IVM worker threads behind a single
writer/coordinator. On a follower the transactions it applies arrive over the
replication stream from the write-master rather than from a client, but the engine
mechanics are identical. The per-transaction handshake keeps the parallelism correct:

1. `write` opens a `BEGIN CONCURRENT` on the writer; the preupdate hook captures the
   row deltas as SQL runs.
2. `commit` fans the captured batch to every worker and waits for each to pin its
   own pre-commit snapshot (the barrier).
3. The writer commits durably while the workers derive their queries' deltas
   **concurrently** under snapshot isolation.
4. Each worker emits its affected queries' deltas, then a progress marker so the
   drain layer knows the transaction is fully delivered.

A query lives on exactly one worker, so **per-query event order is preserved**. If a
worker faults during derivation, that query is torn down (and the pool respawns the
worker) rather than corrupting the stream. The contract is unchanged from the
single-thread path: *view-after-write == fresh-query*. See
[crates](/docs/crates#going-multi-threaded-cluster) for the `Cluster` API.

## The query planner

The daemon runs the cost-based join-flip
[planner](/docs/how-it-works#query-planner) — it annotates each flippable `EXISTS`
with a `flip` decision before lowering, picking 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 server, but it is young. The replica's schema
constraints apply — plain tables (no triggers / generated columns), numbers within
±(2⁵³−1); see [replica and views](/docs/replica-and-views). The bearer token is the
only auth primitive; finer-grained authz lives in your [API tier](/docs/api-server).

This page runs **one** follower. To scale reads across **N** affinity-placed followers fed by the
write-master, see
[deploying & scaling](/docs/deploy), which lays out the whole deployment menu and what
you can run yourself versus have us run for you on
[Rindle Cloud](https://cloud.rindle.sh).

## Next steps

- [The API server](/docs/api-server) — the tier that drives the control plane.
- [The browser client](/docs/client) — what subscribes to the ws plane.
- [`@rindle/cli`](/docs/rindle-cli) — the local supervisor, migration, and schema-gen
  toolchain that ships the daemon binary for JS/TS projects.
- [Full app: the issue tracker](/docs/example-issue-tracker) — `rindled` booted and
  wired to both other tiers with one command.
- [Crates & API map](/docs/crates) — `rindle-replica` (`Db` / `Cluster`),
  `rindle-server`, and `rindle-planner`.
- [Deploying & scaling](/docs/deploy) — from this one node to a read-scaled fleet,
  and what runs it for you on Rindle Cloud.
- [Connect your app to Rindle Cloud](/docs/cloud-connect) — the managed counterpart
  of this page's two planes: the endpoints and token, surfaced on your dashboard.

---

[View this page on Rindle](https://rindle.sh/docs/daemon)
