Rindle keeps a query’s result live. You register a query once and the engine maintains its result as the data changes — computing the incremental difference on each write instead of re-running the query. The core contract is view-after-write == fresh-query: the changes you receive, applied in order, always equal what you’d get by 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
The product: a synced, local-first app
The fullest expression of Rindle is a synced 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
rindleddaemon 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"; // schema + named queries + predicted mutators
export const app = await createRindleClient({
schema,
mutators,
api: { url: "", headers: () => ({ "x-user": currentUser() }) }, // your API authority
daemon: { wsUrl: "ws://localhost:7601" }, // the rindled daemon
onRejected: (envelope, reason) => showToast(`${envelope.name}: ${reason}`),
});
const view = app.store.materialize(queries.issuesPage(null)); // live, local, instant
app.mutate.createIssue({ id, title: "ship it", /* … */ }); // optimistic; rebased on confirm
That’s the whole high-altitude story: write a schema, name your queries, write twin mutators — and live queries, the cache, and optimistic updates are handled. Start with the architecture.
What you stop building
A query that stays correct as the data changes is the primitive under three things every app reinvents — and most get subtly wrong:
- Live queries — no polling, no refetch-on-focus. Materialize a query and it stays current.
- The cache — there is nothing separate to invalidate. The local database is the cache, and the engine keeps every derived view exact.
- Optimistic updates — a named mutator applies in-process instantly; the engine itself rebases it as the server confirms, and a rejected write snaps back — no hand-written rollback.
Write a query, write rows — those three hard parts are handled. (See is Rindle for you? for how this compares to hand-rolled stacks, sync frameworks, and hosted backends.)
The primitive underneath
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. The three-tier app is composed from these homes; you can also use any one on its own:
| Home | Package / crate | What it is |
|---|---|---|
| Browser | @rindle/wasm |
the engine compiled to wasm, queries in-process — reactive reads with no server (~228 kB gz) |
| Node | @rindle/replica |
native SQLite + BEGIN CONCURRENT CDC, in-process |
| Rust | rindle / rindle-replica |
the std-only IVM core, embedded directly |
Just want reactive reads in the browser, no backend? That 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 }));
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.
Choose your path
- Build a synced app → The three-tier architecture, then the client, the API server, and the daemon.
- See it whole → the full issue-tracker example — three tiers, real code, one command.
- Reactive reads in the browser, no server → Reactive queries in the
browser (
@rindle/wasm). - Embedded in Rust → Rust quickstart and the crate map.
Learn the concepts
- How it works — build → lower → hydrate → push, and why incremental beats recompute.
- 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.