# Choose a browser client

Compare the standalone wasm store, remote result client, normalized sync, and optimistic app client.

Rindle has several browser clients. Choose based on where your data lives and
whether the browser predicts writes. React, TanStack, and server rendering are
separate integration choices.

All TypeScript clients use the schema, query builder, `Store`, and view types
from `@rindle/client`. That package is the shared core. Installing it alone does
not create a database or connect to a server.

## Choose by behavior

| You need | Entry point | Browser data | Write behavior |
| --- | --- | --- | --- |
| Live queries over data owned by one browser tab | `createWasmStore` from `@rindle/wasm` | In-memory tables and local query results | Applies local row changes immediately |
| Server-maintained results through a custom flat protocol | `createRemoteStore` from `@rindle/remote` | Materialized query results; no local wasm database | Forwards raw writes; no prediction |
| Synced rows with local queries, without optimistic writes | `createNormalizedStore` from `@rindle/normalized` plus a `NormalizedSource` | A local wasm database populated by retained server queries | Forwards writes through the source; no prediction |
| A full synced app with named mutators | `createRindleClient` from `@rindle/optimistic` | Synced rows, local queries, and pending predictions | Predicts named writes, sends them to the API, then reconciles |
| A custom optimistic transport integration | `createOptimisticStore` from `@rindle/optimistic` plus an `OptimisticSource` | The same local prediction and rebase engine | You supply the authoritative stream and mutation delivery |

For a browser-only application, start with
[Reactive queries in the browser](https://rindle.sh/docs/wasm-client). For the standard Rindle API
server and data tier, start with [`create-rindle`](https://rindle.sh/docs/create-rindle) or
[manual synced-app setup](https://rindle.sh/docs/synced-app-quickstart).

The two middle choices are lower-level compositions. Their transport protocol and
lifecycle must match your server. A WebSocket URL alone is not enough to connect
every client to every Rindle deployment.

## Local engine: no server

`createWasmStore(schema)` creates an independent in-memory database. Add rows with
`store.write`, materialize queries, and subscribe to their current results. All
data comes from your application.

It has no built-in network sync, persistence, or optimistic queue. An immediate
local write is the final write to this database; there is no server verdict to
wait for. See the [standalone walkthrough](https://rindle.sh/docs/wasm-client).

## Remote results: no local engine or prediction

`createRemoteStore(schema, transport)` uses the **flat** result protocol. The
server sends changes to a query result, including nested results. The browser
applies them to an `ArrayView`; it does not store shared base tables or run wasm.

This can suit a thin client when you control the server protocol. Each remote
query needs a registered name and arguments. An arbitrary local builder does
not become a server query automatically.

The production `rindled` WebSocket serves the **normalized** protocol. It does
not serve flat results to `createRemoteStore`, even if you provide a lease
resolver. The repository's private reference server includes a flat server for
integration tests. See [custom remote backends](https://rindle.sh/docs/backends#remote-result-client)
for the boundary and an example.

## Normalized sync: local reads without prediction

`createNormalizedStore(schema, source)` combines the wasm engine with a
`NormalizedSource`. The source supplies changes to the rows and columns needed
by active server queries. The client shares those rows across subscriptions and
computes local views from them.

For WebSocket input, `createRemoteNormalizedSource` from `@rindle/remote` supplies
that source. It understands normalized snapshots and changes. It can subscribe
with an API-issued lease through `resolveSubscribe`.

This composition does **not** include the full app connection lifecycle. Your
integration owns lease requests, endpoint selection, affinity tickets, and
rebuilding subscriptions after a reconnect. A returned `wsEndpoint` does not
move this source's existing transport. `WsTransport` reconnects a socket, but
this source does not automatically re-register its queries on that reconnect.

Writes also need an explicit authority. `store.write` forwards raw mutations
through the source and makes no local prediction. The standard app mutation API
accepts named mutation envelopes, not these raw row operations. Use an application
write endpoint, or implement a compatible `sendMutation` adapter for your own
server. See [normalized composition](https://rindle.sh/docs/backends#normalized-client).

## Optimistic app client: the integrated connection

`createRindleClient` initializes wasm, resolves named query leases through your
API server, and opens the selected data-tier WebSocket. It manages mutation
delivery, reconnects, subscription recovery, and supported endpoint changes.

Call a named mutator to predict a write immediately. Your API server's HTTP adapter verifies
the caller's credentials, and the API server runs the authoritative mutator. As confirmed changes arrive, the
browser reapplies pending mutators to the confirmed rows. This is **rebase**.
Rejected predictions are removed.

The [client guide](https://rindle.sh/docs/client) explains construction, reads, writes, and cleanup.
You can use this client without React. You can also use it for read-only screens
and simply make no mutation calls.

`createOptimisticStore` exposes the engine composition beneath this constructor.
It accepts an `OptimisticSource` and a mutator registry. It does not initialize
wasm or construct your app's HTTP and WebSocket connections for you. Use it when
you are implementing a transport integration; the
[backend guide](https://rindle.sh/docs/backends#custom-optimistic-composition) describes its contract.

## Understand local completeness

A named query describes what the server must supply. A local query over a
normalized or optimistic store reads only the rows currently held in the browser.
It opens no subscription of its own.

For example, a named query that retains 50 issues does not download the whole
issue table. A local count over that table counts available rows. It cannot
establish the server's total. Releasing server queries can also remove rows that
no remaining subscription retains.

Use a named query and its readiness signal when you need an authoritative result.
Use a local query when the available rows are sufficient. See
[preloads and query readiness](https://rindle.sh/docs/preloads).

## Add UI and storage features separately

`@rindle/react` wraps a `Store`; it works with local and remote backends.
TanStack Start adds routing and server integration. Neither choice determines
whether your browser has optimistic writes.

[Server rendering](https://rindle.sh/docs/ssr) uses a separate server store and a browser handoff.
Do not construct the live browser client during a render on the server.

None of these constructors automatically persists the synced database or a
pending mutation queue across reloads. The optimistic app client can persist
explicit local-only tables with `persistLocal`. That option covers UI data such
as drafts, not a durable offline copy of synced data. See
[local tables](https://rindle.sh/docs/local-only-tables) and
[persisting local tables](https://rindle.sh/docs/persisting-local-tables).

[SQL over HTTP](https://rindle.sh/docs/sql-client) is a separate server-code option. Its database
token is trusted, so keep it behind an authorized application endpoint. SQL
returns statement results; it does not create a live view or require an
optimistic client.

---

[View this page on Rindle](https://rindle.sh/docs/browser-clients)
