Start

What is Rindle?

Rindle keeps your queries live with one engine — from a browser tab to a Node server to a synced, local-first app, behind one query language and one correctness contract.

View as Markdown

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-replicator write-master plus one or more rindled read-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

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 / Child vocabulary 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.