Local-only tables hold drafts, selections, and preferences beside a synced client’s server data. They use the same query engine, but Rindle does not send them to the server. Server confirmations do not rewind their contents.
This guide extends the manual synced-app quickstart. A standalone browser store already keeps all its rows local. Local-only tables are temporary unless you enable persistence.
Add a client schema
Keep the generated SQL schema unchanged. Create a separate module for local tables:
// shared/client-schema.ts
import { extendSchema, string, table } from "@rindle/client";
import { schema } from "./schema.gen.ts";
export const draft = table("draft", { local: true })
.columns({ id: string(), issueId: string(), body: string() })
.primaryKey("id");
export const clientSchema = extendSchema(schema, { tables: [draft] });
Use this schema in the quickstart’s browser client:
// src/rindle-client.ts
import { createRindleClient } from "@rindle/optimistic";
import { mutators } from "../shared/app-def.ts";
import { clientSchema } from "../shared/client-schema.ts";
const currentUser = () => "demo";
export const app = await createRindleClient({
schema: clientSchema,
mutators,
user: currentUser,
api: { url: "", headers: () => ({ "x-user": currentUser() }) },
onRejected: (envelope, reason) => window.alert(`${envelope.name}: ${reason}`),
});
The x-user header is the quickstart’s development identity. A deployed app
must use its authenticated session. The API server and named queries continue
to import the generated schema; extendSchema accepts only local tables.
Read and write a draft
This helper owns one live query. It inserts the first draft and edits later values:
// src/drafts.ts
import { app } from "./rindle-client.ts";
export function openDraft(issueId: string) {
const view = app.store.query.draft.where.id(issueId).materialize();
return {
view,
setBody(body: string) {
const previous = view.data[0];
return app.store.writeLocal((tx) => {
const next = { id: issueId, issueId, body };
if (previous) tx.edit("draft", previous, next);
else tx.add("draft", next);
});
},
close() { view.destroy(); },
};
}
const draft = openDraft("issue-42");
const unsubscribe = draft.view.subscribe(() => {
console.log(draft.view.data[0]?.body ?? "");
});
await draft.setBody("A reply in progress");
// When this consumer is finished:
unsubscribe();
draft.close();
view.data holds the current result. Use a subscription or a
React hook to update a UI when that result changes.
Destroy a materialized view when its owner is finished.
writeLocal resolves after the local write. It does not wait for IndexedDB
storage or send an authoritative mutation.
Keep local state outside the mutation contract
writeLocalaccepts only local tables. Use a named mutator for synced tables.- A mutator cannot read or write local tables. Its server execution and rebase must not depend on private browser state.
- A named server query cannot reference a local table. Local queries can combine local rows with synced rows already available in the client.
Choose a lifetime
| Declaration | Without persistence | With persistLocal |
|---|---|---|
{ local: true } |
Memory for this client | IndexedDB and coordination across tabs |
{ local: "session" } |
Memory for this client | Still ephemeral and per-tab |
Use local: "session" for a transient selection. Use local: true for a draft
that you may want to restore. Neither declaration sends its rows to the server.
See Persisting local tables for storage identity, restore behavior, and cleanup on logout.