Railbase
GPTClaude

TypeScript SDK

Generate a fully-typed client for your collections, auth, and realtime.

Updated

Video guide —watch on YouTube ↗

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.

Was this page helpful?Thanks for your feedback!