Typed server-side data
The browser client is typed by passing your generated Database to
createRebaseClient<Database>() (see Typed SDK Generation).
The server-side data surfaces have no such call to pass it to:
context.datain a collection callback,rebase.dataAsAdminon therebasesingleton,- the
rebasea custom function or a cron job is handed.
So you tell TypeScript once, for the whole project, which Database they serve.
Until you do, they stay untyped: any collection by any name, rows as
Record<string, unknown>. Nothing changes for a project that registers nothing.
Register the Database
Section titled “Register the Database”Since 0.25
rebase generate-sdk --registerBesides the SDK in generated/sdk/, this writes two files to
config/generated/: a copy of database.types.ts, and a register.ts that
declares the registration:
import type { Database } from "./database.types";
declare module "@rebasepro/types" { interface Register { database: Database; }}They live in config/ because your collection files are compiled twice, by the
backend (whose tsconfig.json includes ../config/) and by the config
package’s own tsc, and config/ is the one directory both read. A
registration there that imported ../generated/sdk/ would pull a file from
outside config/ into that package’s compile, which TypeScript refuses. So one
registration types your functions, your cron jobs and the callbacks in your
collection files, in both compiles. It is a declaration only: the runtime does
exactly what it did before.
The flag is remembered in rebase.json, as "registerDatabase": true on the
backend app, so every later rebase generate-sdk, and rebase dev on each
collection change, keeps both files current. That holds on a fresh clone and in
CI too: the scaffold’s .gitignore ignores generated/ directories, and the
first rebase generate-sdk or rebase dev writes them again. A project with
no rebase.json keeps registering for as long as config/generated/register.ts
is there. To go back to untyped server-side data, remove registerDatabase and
delete config/generated/.
You can also write the registration yourself, in any file the backend compiles
(backend/src/rebase-env.d.ts, importing ../../generated/sdk/database.types).
It then reaches the backend’s compile only: in the config package’s own tsc,
context.data stays the untyped object, so callbacks have to compile both ways.
What it types
Section titled “What it types”// config/collections/orders.ts — a callbackcallbacks: { async beforeSave({ context, values }) { const { data } = await context.data.customers.find({ where: { email: ["==", "ada@example.com"] }, // `email` is a column of customers }); data[0]?.email; // completes from the customers Row
await context.data.customer.find(); // error: no collection `customer` return values; }}// backend/functions/stats.tsimport { defineFunction } from "@rebasepro/server/functions";
export default defineFunction((app, { rebase }) => { app.get("/", async (c) => { const { data } = await rebase.dataAsAdmin.orders.find({ where: { status: ["==", "paid"] }, // `status` and its values are checked }); return c.json({ paid: data.length }); });});// backend/crons/cleanup.tsimport { defineCron } from "@rebasepro/server";
export default defineCron({ name: "Cleanup", schedule: "0 3 * * *", async handler({ rebase }) { // `create` takes the collection's Insert type: required columns are required. await rebase.dataAsAdmin.auditLog.create({ action: "cleanup" }); },});Each accessor is typed the way the browser client’s is: find, findById
and listen return the collection’s Row; where and orderBy are
keyed by its columns; include names its relations; create takes its
Insert and update its Update. Accessors are the camelCase names from the
generated file (order_items is orderItems).
The server resolves those names through the registered collections, so a
my-notes collection stored in a notes table is context.data.myNotes.
collection(slug) with a slug the Database declares is typed the same way:
context.data.collection("my-notes") is context.data.myNotes. A slug you
compute at runtime, or one the schema does not declare, gets the untyped
client.
One function, without the registration
Section titled “One function, without the registration”defineFunction and defineCron take the type as an argument too, which wins
over the registration. Use it to type a single file, or to try typed data before
registering:
import type { Database } from "../../generated/sdk/database.types";
export default defineFunction<Database>((app, { rebase }) => { // rebase.dataAsAdmin is typed from Database here});Helpers that take the data object
Section titled “Helpers that take the data object”A helper that receives context.data or rebase.dataAsAdmin names the
registered type, so it stays typed and accepts both:
import type { RebaseSdkData, RegisteredDatabase } from "@rebasepro/types";
export async function paidOrders(data: RebaseSdkData<RegisteredDatabase>) { return data.orders.find({ where: { status: ["==", "paid"] } });}A helper declared with a bare RebaseSdkData keeps the untyped type, and a
typed data object is not assignable to it. Change the parameter as above when
you register.