This page describes the standard optimistic synced app built with
createRindleClient, createRindleApiServer, and a Rindle data tier.
The scaffold and manual quickstart
use this composition.
Rindle also works without this architecture. An embedded Rust database needs no server. A standalone browser store needs no network. Other browser clients stream results or maintain local rows without optimistic prediction. React, TanStack Start, and SSR are optional.
The three tiers
| Tier | Owns | Does not own |
|---|---|---|
| Browser | Local query results, pending predictions, connection lifecycle | Authoritative access decisions or database credentials |
| Application API | Query definitions, argument validation, access policies, authoritative mutators | A persistent copy of the database |
| Data tier | Durable rows, SQL transactions, maintained queries, subscription delivery | Your application’s user authentication |
Your HTTP handler authenticates the request and passes its verified user to the
API handlers. createRindleApiServer runs the policies you configure. Importing
this package does not add an authentication provider or discover row-level rules.
The API server can run in your existing application server or a serverless function. The data tier remains available between requests. The browser receives rows through a separate WebSocket connection authorized by the API server.
The shared app contract
A typical project shares three kinds of definitions:
- Schema: TypeScript table and column definitions generated from the database, plus application-owned relationships and type refinements.
- Named queries: a stable name, an argument parser, and a query builder. The API server registers these definitions and can add user-specific restrictions.
- Shared mutators: argument parsers and write logic that the browser can predict and the server can execute authoritatively.
A shared mutator is optional when reads are all you need. An optimistic app uses mutators for writes to synced tables. It can also have server-only writers and local-only tables, which follow different write paths.
The manual quickstart defines every file for this contract. Schema, queries, and mutators explain how to extend it.
Reads and writes
Subscribing to a query
- The browser materializes a named query with its arguments.
- The client sends that name and those arguments to the application’s query endpoint.
- Your HTTP handler authenticates the caller. The API server validates the query arguments and applies the configured access policy.
- The API server asks the data tier to maintain the approved query.
- The data tier returns a lease: permission to subscribe to that query, with an expiry and a public WebSocket endpoint.
- The browser presents the lease and receives an initial snapshot, then changes.
The standard daemon stream carries normalized rows, identified by table and primary key. Overlapping subscriptions share local rows. The browser’s WASM engine maintains the final query results over that local data. Data slices can arrive before commit; the client stages them until the stream’s progress confirms them.
A bare store.query on this client reads rows already present locally. It does
not request more server data. A named query requests an authorized server
subscription. Offline queries can only use rows the client still holds.
Making a write
- An event handler creates any IDs or timestamps and calls a named mutator.
- The client runs the mutator against local rows and shows the predicted result.
- The client sends a mutation envelope containing its identity, sequence number, mutator name, and arguments to the application API.
- The server validates and authorizes the operation, then runs its authoritative implementation in a database transaction.
- The data tier streams row changes and sends commit progress to the browser.
- The client removes confirmed predictions and replays the remaining pending mutators.
This replay is called rebase. Client and server results can differ: the server can see rows the browser lacks, or reject a write. Your UI handles loading, pending work, and rejections.
Client-generated SQL, ASTs, and predicted row effects do not establish authority. The server resolves names and arguments through its own registry and policies.
Where SQL fits
Database-backed apps define tables through SQL migrations. The
SQL client also supports ordinary reads and transactions.
A SQL SELECT returns a response; it does not create a live subscription.
A job or existing service can write to the authoritative database through the supported background write path. The data tier captures those changes and updates affected subscriptions. Such writes do not predict anything in the browser.
When the browser predicts a write, use the optimistic mutation protocol for its server execution. An unrelated SQL request does not acknowledge that client’s pending mutation.
The daemon’s two planes
A standard rindled exposes separate public and private interfaces:
| Interface | Caller | Purpose |
|---|---|---|
| Public WebSocket | Browser with an authorized lease | Initial normalized rows and live changes |
| Private HTTP control | Application API with server credentials | Materialize, read, and manage queries |
| SQL HTTP | Trusted server or script with a database token | SQL reads and writes allowed by the deployment |
The browser calls your application’s API routes. It does not receive the
RINDLE_DATABASE_TOKEN or private daemon credentials.
One standalone daemon can own the SQLite database, writer, and live queries. A replicated deployment separates the write master from read followers. The deployment guide explains those choices. They do not change the application’s named-query and mutator contract.
PostgreSQL integration uses another topology: Postgres remains authoritative, and a gateway sends its changes to Rindle followers. It is a preview with its own setup and recovery constraints.
Where server rendering fits
SSR reads a query once under the current request’s authority. It puts that result in the initial page. This seed is a snapshot of query results, not a persisted browser database. The live client still establishes its subscriptions.
The SSR guide defines the server read, shared query, browser boot function, and React boundary. The TanStack adapter connects those pieces to route loading. Neither SSR nor TanStack is required for client-only rendering.
The correctness contract
The engine’s contract is view-after-write == fresh-query. After it applies a change, its maintained view equals a fresh query over the same underlying data.
This does not mean that every browser instantly sees the server’s latest commit. Network delivery can lag, and an optimistic view can include unconfirmed writes. The sync protocol reconciles those states as authoritative updates arrive.
Next steps
- Manual quickstart: build this composition with explicit files.
- Optimistic client: understand local reads, mutation progress, and cleanup.
- API server: configure query and mutation authority.
- Browser client choices: choose a different composition.
The implementation lives in packages/optimistic/src/client.ts,
packages/api-server/src/index.ts, and rust/rindle-server/src/net/mod.rs.
One-shot reads and subscription leases use the API server’s query authorization
and resolution path. Mutations have their own authorization and transaction path.