# Backends & the homes

One engine, one query API, one correctness contract — over wasm in the browser, native SQLite in Node, or a server. The Backend seam that lets one view serve every tier.

The JS client is built around one seam: a **`Backend`**. The core package
`@rindle/client` owns everything *above* the seam — the typed schema, the fluent
query builder, the comparator, the `Store`, and the `ArrayView` that folds a change
stream into a live, materialized tree. A `Backend` owns everything *below* it —
where the engine actually runs.

Swap the backend, keep the code. The schema you write, the query you build, the view
you subscribe to, and the contract you rely on (*view-after-write == fresh-query*)
are **identical** in every home. This page is the conceptual underpinning; for the
full synced app the homes compose into, start with
[the architecture](/docs/architecture).

## The homes

| Home | Package | Engine | Reads |
| --- | --- | --- | --- |
| **Browser** | [`@rindle/wasm`](/docs/wasm-client) | the IVM engine compiled to wasm, in-process | synchronous, local |
| **Node** | [`@rindle/replica`](/docs/replica-and-views) | native SQLite + `BEGIN CONCURRENT` CDC, in-process | synchronous, local |
| **Server (network)** | [`@rindle/optimistic`](/docs/client) → [`rindled`](/docs/daemon) | a daemon over a WebSocket, behind your API tier | eventually consistent, then local |
| **Rust** | [`rindle`](/docs/crates), [`rindle-sqlite`](/docs/crates) | the std-only core, embedded directly | — (the engine itself) |

The browser and Node backends are **local** — the engine is in your process, so a
write applies and the affected views update before `store.write` resolves. The
**server** home runs the engine on the [`rindled` daemon](/docs/daemon) and streams
changes to subscribers; in its richest form (the
[synced client](/docs/client)) the browser *also* runs a local engine over those
streamed rows, so reads are instant and writes optimistic.

## What a backend does

A `Backend` is a small interface. The `Store` drives it; you rarely implement one
yourself.

```ts
interface Backend {
  registerQuery(qid: QueryId, ast: Ast): void;     // start a live query
  unregisterQuery(qid: QueryId): void;             // tear it down
  mutate(mutations: Mutation[]): Promise<void>;    // apply writes
  onEvent(handler: (qid: QueryId, ev: ChangeEvent) => void): void;
}
```

Every backend emits the **same** per-query `ChangeEvent` stream, in three shapes:

- **`hello`** — the schema + comparator version for this query (the view resets to it).
- **`snapshot`** — the initial result set, as a batch of adds.
- **`batch`** — the incremental changes for one committed write.

The `Store` builds one `ArrayView` per query and routes that query's events into it.
Because the stream shape is identical, **one `ArrayView` implementation folds every
backend's output** — the wasm engine's, the native replica's, or a remote server's.

```ts
const store = new Store(schema, backend); // backend = WasmBackend | ReplicaBackend | …
const view = store.query.issue.where.closed(false).materialize();
view.subscribe(render);
```

## Local vs. remote, precisely

A **local** backend (`@rindle/wasm`, `@rindle/replica`) runs the engine in-process and
emits a lossless stream: `hello` → `snapshot` → `batch`, with no gaps to recover
from. `materialize()` comes back already hydrated.

A **remote** backend speaks the same `ChangeEvent` stream to the `Store`, but
underneath it owns a wire protocol: it validates an epoch / sequence /
fingerprint-stamped frame stream from the server, and on a gap or drift it
re-subscribes. The server re-registers under a new epoch and sends a fresh
snapshot, and the `ArrayView` **resets in place**, so the view reference you hold
survives. The `Store` and `ArrayView` never see any of that; they just receive a
clean stream. (This split — validation in the remote backend, folding in the core
view — is what lets one view serve both.)

## The synced backend composes two homes

The seam composes. The [synced browser client](/docs/client) (`@rindle/optimistic`,
wired by `createRindleClient`) is a backend built from *two* homes at once: a
**local** wasm engine for instant reads and optimistic writes, with its base tables
fed by a **remote** [daemon's](/docs/daemon) normalized stream. Writes are named
mutators the local engine applies instantly and *rebases* as the server confirms — a
rejected write snaps back on its own. The `Store` and `ArrayView` above the seam
don't change — they materialize from the local engine, which is itself kept current
by the network. That's how one client gets local-engine speed *and* server authority
behind the same `materialize()`.

## Why this matters

You prototype against the browser engine, ship the same components against a Node
replica, and move queries to the [synced client](/docs/client) without touching a
line of query code. The hard part — keeping a materialized result correct under
incremental change — is solved once, in the engine, and reused at every tier.

## Next steps

- [The three-tier architecture](/docs/architecture) — the full synced app the homes
  compose into.
- [Reactive queries in the browser](/docs/wasm-client) — the `@rindle/wasm` engine on
  its own, no server.
- [The live-replica runtime](/docs/replica-and-views) — the commercial high-concurrency
  layer: SQL writes in, deltas out, readers scaled.
- [The change model](/docs/change-model) — the `Add` / `Remove` / `Edit` / `Child`
  deltas the stream carries.
- [Fold the delta stream yourself (Rust)](/docs/example-rust) — the seam at its
  lowest level.

---

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