Register a query once and the engine maintains its result as the data changes — recomputing only what each write affects, never the whole query. Its core contract is view-after-write == fresh-query: the deltas you receive, applied in order, always equal running the query from scratch.
The engine is rindle, an incremental view maintenance (IVM) engine written in
Rust. Its one trick is the thing read models, caches, and dashboards are forever
reinventing:
re-run on every write -> cost grows with the table
maintain on every write -> cost grows with the change
Rows in, view out
That’s the whole mental model: rows change in, the view updates out. Query once, maintain forever. It’s a small primitive you can drop into what you already have — no networking, no sync, no architecture to adopt — and it already solves problems most apps hand-roll:
- expensive recomputation and cron-refreshed tables;
- cache invalidation (there is no separate cache — the view is the result);
- read models and derived state;
- live dashboards and materialized views.
The lowest-friction taste is the engine compiled to wasm, running in-process in a browser tab. The same engine is the whole API — four steps: schema, store, query, write.
import { table, string, number, boolean, createSchema, createWasmStore } from "@rindle/wasm";
const issue = table("issue")
.columns({ id: number(), title: string(), closed: boolean() })
.primaryKey("id");
const store = await createWasmStore(createSchema({ tables: [issue] }));
const view = store.query.issue.where.closed(false).orderBy("id", "desc").materialize();
view.subscribe((rows) => render(rows)); // fires now, then after every write that affects it
await store.write((tx) => tx.add("issue", { id: 1, title: "first", closed: false }));
Here the schema is written in TypeScript because this in-tab engine has no database — the
rows come from tx.add. The moment your data lives in SQLite (a daemon or a
synced app), SQL is the source of truth: you define tables with ordinary DDL and
generate this same schema from them with rindle schema gen. The typed builder is a facade
over your tables, never where they’re defined — see schema & migrations.
One engine, every home
The same engine runs wherever your data lives, behind one schema and one query language — Deltic, a fluent builder in Rust or JS — with one materialized-view contract. Use any one home on its own, or compose them:
| Home | Package / crate | What it is |
|---|---|---|
| Browser | @rindle/wasm |
the engine compiled to wasm, queries in-process — reactive reads with no server (~200 kB gz) |
| Node | @rindle/replica |
native SQLite + BEGIN CONCURRENT CDC, in-process |
| Rust | rindle / rindle-replica |
the std-only IVM core (open source), embedded directly; rindle-replica adds the live-replica runtime (commercial) |
Start with the engine in-process (Level 1); run it as the always-up rindled
daemon when you want the same live queries shared across processes (Level 2). The
core is open source (Apache-2.0): the rindle engine crate, the rindle-sqlite
backend, and the JS/TS packages, at
github.com/rindle-sh/rindle. The
high-concurrency server runtime — rindle-replica, rindled, and the replicated
fleet — is commercial and ships as prebuilt binaries. Both routes are just the
engine — use the engine →.
The change model
You never receive a re-fetched result — you receive the delta. A change event is
one of four shapes: a row entered (Add), left (Remove), changed in place
(Edit), or a nested relationship of an in-view row changed (Child). There is no
Insert / Delete / Update — those are the writes you make, not the deltas you
receive. The JS view folds the stream for you; applying it reconstructs exactly a
fresh query — see the change model.
Scale up to a synced app (Level 3)
Because the same engine runs in the browser and on the server, the most sophisticated thing it does falls out as a consequence, not a prerequisite: a synced, local-first app in three tiers — the same shape as client / stateless app server / database, but live end to end.
- a browser client that runs its own engine over its own local database: reads resolve locally and instantly, writes apply optimistically;
- a stateless API server that is your app’s authority — it resolves named queries and runs the authoritative mutators;
- an always-up Rindle data tier — a
rindle-replicatorwrite-master plus one or morerindledread-followers (the smallest is a colocated pair on one box) — that holds the data and the live queries and streams normalized updates to every subscriber.
The client is one call:
import { createRindleClient } from "@rindle/optimistic";
import { mutators, schema } from "./app-def.ts"; // schema + isomorphic mutators
import { issuesPageQuery } from "./IssueListItem.queries.ts"; // a co-located named query
export const app = await createRindleClient({
schema,
mutators,
user: () => currentUser(), // a mutator's ctx.user
api: { url: "", headers: () => ({ "x-user": currentUser() }) }, // your API authority
daemon: { wsUrl: "ws://127.0.0.1:7601" }, // the rindled follower's public ws
onRejected: (envelope, reason) => showToast(`${envelope.name}: ${reason}`),
});
const view = app.store.materialize(issuesPageQuery({ limit: 50 })); // live, local, instant
app.mutate.createIssue({ id, title: "ship it", /* … */ }); // optimistic; rebased on confirm
At this altitude the engine takes over three things every app reinvents and most get subtly wrong — live queries, the cache, and optimistic updates — with no polling, nothing to invalidate, and no rollback code. When you want it, start with the architecture.
Choose your path
- Use the engine (Levels 1–2, start here) → start with how it works, then reactive reads in the browser, the Rust quickstart, replica & views, and the crate map.
- Build a synced app (Level 3) → start with
the synced-app quickstart, scaffold a TanStack app with
create-rindle, then read the architecture, the client, the API server, and the daemon — or see it whole.
Learn the concepts
- How it works — build → lower → hydrate → push, and why incremental beats recompute.
- Schema & migrations — SQL is the source of truth; migrate it and generate the typed schema your query builder needs.
- The change model — the
Add/Remove/Edit/Childvocabulary and the replay-equivalence invariant. - Supported queries — the honest matrix of what builds, pushes, and materializes today.
- Performance — sub-microsecond incremental maintenance that stays flat as data grows, measured end to end.
- Is Rindle for you? — what it replaces and how it compares.