Typed SDK — Getting Started
Overview
Section titled “Overview”The @rebasepro/client package provides a type-safe JavaScript SDK for interacting with your Rebase backend. It handles:
- Data operations — CRUD with filtering, sorting, and pagination
- Relation fetching — Include related entities with
.include() - Real-time subscriptions — WebSocket-based live updates
- Offline & local-first sync — Opt-in local row database, instant offline writes, live queries
- Authentication — Token management, login, signup, OAuth
- Storage — File upload, download, and management
- Custom functions — Call custom server endpoints
Installation
Section titled “Installation”pnpm add @rebasepro/clientCreating a Client
Section titled “Creating a Client”rebase dev derives a free port from the project’s path rather than using a
fixed one, so read baseUrl from the URL it printed — there is no port
every project shares. In a Vite frontend that is the VITE_API_URL the scaffold
writes into .env; in a script, an environment variable of your own.
import { createRebaseClient } from "@rebasepro/client";
const client = createRebaseClient({ baseUrl: import.meta.env.VITE_API_URL,});The websocketUrl is derived automatically from baseUrl (http → ws, https → wss). You can override it explicitly if needed:
const client = createRebaseClient({ baseUrl: import.meta.env.VITE_API_URL, websocketUrl: import.meta.env.VITE_WS_URL,});Configuration Options
Section titled “Configuration Options”| Option | Type | Description |
|---|---|---|
baseUrl |
string |
Backend URL. Read it from what rebase dev printed, or from your deployment |
websocketUrl |
string |
WebSocket URL — auto-derived from baseUrl if omitted |
token |
string |
Static JWT token for server-to-server calls |
apiPath |
string |
API prefix (default: "/api") |
fetch |
typeof fetch |
Custom fetch implementation (e.g. for SSR) |
onUnauthorized |
() => Promise<boolean> |
Custom 401 handler — return true to retry |
realtime |
boolean |
Open the WebSocket (default true) — set false in one-shot scripts |
collections |
Record<string, string> |
Maps accessor names to collection slugs |
offline |
boolean | OfflineConfig |
Local-first sync — off by default |
impersonate |
string |
Run every request as this user, by uid, to check what one user can see. Only an administrator’s session may: the data API, functions and realtime run as that user, and every other route refuses |
Typed SDK Generation
Section titled “Typed SDK Generation”Generate a fully typed client from your collection definitions:
rebase generate-sdkWhile rebase dev is running you do not need to: it regenerates the types in
generated/sdk/ on start and on every save under config/collections/. Run the
command yourself in CI, in a frontend repository that has no collections
(rebase generate-sdk --from link), or anywhere rebase dev is not running.
On 0.23, rebase dev leaves the SDK types alone, so run the command after
every collection change.
Then pass the Database type parameter to createRebaseClient for full autocomplete:
import { createRebaseClient } from "@rebasepro/client";import { collectionsDictionary, type Database } from "./generated/sdk/database.types";
const client = createRebaseClient<Database>({ baseUrl: import.meta.env.VITE_API_URL, collections: collectionsDictionary,});
// Full autocomplete on collection names and field typesconst { data } = await client.data.products.find();When Database is supplied, createRebaseClient returns a CreateRebaseClientResult<DB> instance. This maps camelCase collection accessors directly on client.data to their corresponding types, giving you full autocomplete on collection operations and types (e.g. client.data.products.find()).
collectionsDictionary maps each accessor back to the slug the wire uses. Pass it whenever a slug is not already a valid property name — my-notes is reachable as client.data.myNotes only because the dictionary says so.
A few names on client.data are taken: collection is the dynamic collection(slug) function, and then, toJSON and $$typeof are left alone for promise and serialisation checks. A collection whose accessor would be one of them is generated as <name>Collection — the collection slug is client.data.collectionCollection — and generate-sdk prints a warning naming both. client.data.collection("collection") reaches it too.
Field names
Section titled “Field names”A field’s name on the wire is its property key, and the API is camelCase throughout. A createdAt property stored in a created_at column is row.createdAt, and a relation’s foreign key is authorId even though the column stays author_id. where and orderBy are keyed off the same Row type, so what compiles is what the backend answers to.
A property key you wrote is your key, whatever its shape — nothing renames a name you chose. The two keys that are derived rather than declared, a relation’s foreign key and a column read back by introspection, are camelCase.
Row describes a read, Insert a create() and Update an update() — they are not the same shape. A column that is not required is T | null on all three, so a write can clear it; the primary key is always present on a read and never settable on an update; a belongsTo target can be written either as the relation ({ author: 5 }) or as its foreign key ({ authorId: 5 }); and a hasMany or manyToMany membership is written as a list of target ids or { id } objects ({ tags: ["t-1"] }), each carrying a _pivot when the junction declares payload columns. A field nobody may write (access: { write: [] }) is on neither Insert nor Update. A date column reads as an ISO string, and a write takes either that string or a Date.
Quick Example
Section titled “Quick Example”// Createconst product = await client.data.products.create({ name: "Camera", price: 299,});
// Query with filtersconst { data } = await client.data.products .where("price", ">=", 100) .orderBy("createdAt", "desc") .limit(10) .find();
// Real-time subscriptionconst unsubscribe = client.data.products.listen( { where: { active: ["==", true] } }, (response) => console.log("Updated:", response.data));Using with React
Section titled “Using with React”In a Rebase frontend, the client is created once and shared via context:
import { createRebaseClient } from "@rebasepro/client";
const client = createRebaseClient({ baseUrl: API_URL });
<Rebase client={client} ...>Access it from any component:
import { useRebaseClient } from "@rebasepro/app";
function MyComponent() { const client = useRebaseClient(); // client.data, client.auth, client.storage, client.functions}Next Steps
Section titled “Next Steps”- Querying Data — CRUD, filters, pagination, and relations
- Authentication — Sign in, sign up, OAuth, sessions
- Realtime Subscriptions — Live data with WebSockets
- Offline & Local-First Sync — Work without a connection, and sync when it returns
- Storage & Files — Upload, download, and manage files