TypeScript SDK
Generate a fully-typed client for your collections, auth, and realtime.
Updated
Railbase generates a TypeScript client from your schema, so your frontend gets typed CRUD, auth, and realtime with autocomplete — no hand-written API layer and no drift between client and server.
Generate the client
./myapp generate sdk --out ./web/src/client
Run this with your project's binary (./myapp), not the stock railbase
binary — the schema registry is populated at compile time by your project's
schema package, so the distributed railbase binary has no collections and
generate sdk there errors with "no collections registered — import your
schema package from main.go".
This emits a self-contained client — an index.ts entry, types.ts, zod.ts,
a collections/<name>.ts per collection, plus auth.ts, realtime.ts,
schema-independent modules (account.ts, tenants.ts, notifications.ts,
i18n.ts, stripe.ts, contact.ts, errors.ts) and a _meta.json used for
drift detection. It's a full regenerate each time; commit the output and
never hand-edit it.
Tip
Add --check in CI to fail the build when the committed client drifts from the
schema (./myapp generate sdk --check exits non-zero on mismatch). Or let
railbase dev regenerate it as you code — --watch-schema <dir> takes the
directory to watch and requires --sdk-pkg <path> (the schema package to
re-run against), e.g. railbase dev --watch-schema ./schema --sdk-pkg ./schema.
Use it
import { createRailbaseClient } from "./client";
export const rb = createRailbaseClient({
baseURL: "", // same-origin in prod; "http://localhost:8095" in dev
storage: window.localStorage, // persist the bearer token across reloads
storageKey: "rb_token", // same key plugin frontends use by convention
});
// Typed CRUD — `posts` is generated from your collection
const { items } = await rb.posts.list({ filter: "status='published'", sort: "-created" });
const post = await rb.posts.create({ title: "Hello", status: "draft" });
// Auth — a `<name>Auth` helper per auth collection. The built-in
// `_users` is always generated. Sign-in returns the token; the client
// does NOT capture it automatically — hand it to setToken() to persist
// and start sending it:
const { token, record } = await rb._usersAuth.signinWithPassword({
identity: "you@example.com",
password: "…",
});
rb.setToken(token); // persists to `storage` and attaches to every request
await rb._usersAuth.signup({ email, password, passwordConfirm });
const me = await rb.me();
// Realtime — `subscribe` returns a typed async iterator; iterate with
// `for await`, and abort to stop.
const ctrl = new AbortController();
for await (const ev of rb.realtime.subscribe({ topics: ["posts/*"], signal: ctrl.signal })) {
console.log(ev.id, ev.topic, ev.data);
}
// later: ctrl.abort()
Once you call rb.setToken(token), the client persists it to the storage you
passed (and rehydrates it on the next reload) and attaches it to every request;
call rb.setToken(null) on logout and rb.setTenant() to change tenant at
runtime. The token is not captured from auth responses automatically — that
setToken call after sign-in is required. Realtime subscriptions use the
client's streaming fetch helper so the same Authorization header and tenant
selection are applied to /api/realtime.
Other generators
railbase generate openapi --out openapi.json # OpenAPI 3.1 spec
railbase generate schema-json --out schema.json # machine-readable schema (for tooling)
Note
The SDK generator targets TypeScript. Other languages can consume the OpenAPI spec above.