You provisioned an app on Rindle Cloud (if not, start with the Cloud quickstart). Now you point your two tiers at it: the API server that holds your app’s authority, and the browser client your users run. Nothing about your app code changes — the dashboard supplies connection coordinates and server-side credentials for the same SDK you’d use self-hosted.
The values on your dashboard
Open your app and find the Connect panel. It surfaces exactly what the two tiers need:
| Value | Who uses it | Goes into |
|---|---|---|
Live queries — wss://… |
the browser | createRindleClient({ daemon: { wsUrl, affinity: true } }) |
Control plane — https://…:8443 |
the API server (reads) | the reads leg — new HttpRindleDaemonClient({ baseUrl }) |
Write-master — https://… |
the API server (writes) | the writes leg of the SplitDaemonClient |
| Daemon token | the API server | the Bearer header on the reads leg |
| Replicator token | the API server | the Bearer header on the writes leg |
Every app is a rindle-replicator write-master plus one or more rindled
read-followers, so the API server has two legs: reads to a follower’s Control plane,
writes to the write-master. The split is the whole security model: the token never
reaches the browser. Only your API server holds it, and it dials the data tier’s
bearer-gated private control plane. The browser only ever gets the
public, lease-gated WebSocket — it can subscribe, but every query is authorized
by your API server first.
The tokens are per-app secrets. The dashboard masks them until you click reveal, and the snippets read it from an environment variable rather than inlining it — put it in your API server’s secret store (
RINDLE_DAEMON_TOKENbelow), not in source control.
Your API server → the daemon
This is the API server page’s wiring with the baseUrl and the
token swapped for your dashboard’s Control plane endpoint and your
RINDLE_DAEMON_TOKEN. Everything else — your queries, your shared mutators,
your authorizers — is unchanged:
import { HttpRindleDaemonClient } from "@rindle/daemon-client";
import { createRindleApiServer, SplitDaemonClient } from "@rindle/api-server";
import { schema, queries, mutators } from "./app-api.ts"; // your registerQueries + sharedApiMutators (server/api.ts in the quickstart)
// The API server is the only tier that holds the token — it never reaches the browser.
// One topology: reads → the follower (Control plane), writes → the write-master.
const reads = new HttpRindleDaemonClient({
baseUrl: "https://rindle-ab12cd34.fly.dev:8443", // ← Control plane (reads), from your dashboard
headers: { authorization: `Bearer ${process.env.RINDLE_REPLICATOR_TOKEN}` },
});
const daemon = new SplitDaemonClient(
new HttpRindleDaemonClient({
baseUrl: process.env.RINDLE_REPLICATOR_URL!, // ← Write-master (writes), from your dashboard
headers: { authorization: `Bearer ${process.env.RINDLE_DAEMON_TOKEN}` },
}),
reads,
);
export const api = createRindleApiServer({
daemon, schema, queries, mutators,
// …plus the same authorizeQuery / authorizeMutation you already had — unchanged.
});
// mount api.routes on your HTTP host (node:http, a Worker, Lambda, Hono, …)
Because @rindle/api-server is
transport-agnostic, the API server can run
anywhere that speaks HTTPS — a Cloudflare Worker, a Node process, a Lambda. It
reaches the follower’s Control plane (reads) and the write-master (writes) over TLS
with the bearer token; you don’t run anything inside Rindle Cloud to make those calls.
Your browser client → your API server
The client connects to your API server for query leases and mutations, and opens its live subscription against the dashboard’s Live queries endpoint:
import { createRindleClient } from "@rindle/optimistic";
import { schema, mutators } from "./shared/rindle";
const app = await createRindleClient({
schema,
mutators,
user: () => currentUser(), // a mutator's ctx.user
api: { url: "" }, // your own API server (above), same-origin
daemon: {
wsUrl: "wss://rindle-ab12cd34.fly.dev", // ← stable fleet endpoint
affinity: true, // keep ws + lease on one follower
},
});
That’s the entire change from local development: the follower’s wsUrl and the API
server’s read/write endpoints move from 127.0.0.1 to the endpoints on your dashboard.
What changes from self-hosted
If you followed the self-host docs first, only the connection coordinates differ — your queries, mutators, schema, and React code are identical:
| Self-hosted (local) | Rindle Cloud | |
|---|---|---|
API server reads (RINDLE_DAEMON_URL) |
http://127.0.0.1:7600 |
the Control plane endpoint (:8443, TLS) |
API server writes (RINDLE_REPLICATOR_URL) |
http://127.0.0.1:7611 |
the write-master endpoint |
Client daemon.wsUrl |
ws://127.0.0.1:7601 |
the Live queries endpoint (wss://) |
| Daemon token | one you set on the pair | minted per app, on your dashboard |
| Replicator token | one you set on the master | minted per app, on your dashboard |
| Running the data tier | you operate the pair | operated for you |
Multi-node apps
Every managed app is a rindle-replicator write-master plus one or more separately
placed rindled read-followers: reads go to the Control plane / Live queries
endpoints, writes to the write-master. Scaling from one follower to a regional fleet
keeps the same connection shape:
- Reads keep using one stable Live queries endpoint. With
affinity: true, a follower mints a signed placement ticket; the WebSocket and lease request are then replayed to that same follower. A sustained outage clears the ticket and re-pins the client to a live follower. - Writes keep using the separately bearer-gated Write-master endpoint from the dashboard. They still pass through your API server and never expose the replicator token to the browser.
The reason there’s one write master at all is the correctness contract: one total write order is what lets every follower and every browser converge without a conflict-resolution protocol. The master accepts concurrent transactions and commits them into that one order; “one master” does not mean one request at a time.
Next steps
- The API server — resolving named queries and running authoritative mutators, in full.
- The browser client — optimistic writes, live views, and rebase.
- Scale & operate — the dashboard, resizing, and what each profile costs.
- Full app: the issue tracker — the same two tiers in a complete React app.