Backend Overview
Overview
Section titled “Overview”The Rebase backend is a Node.js server built on Hono that provides:
- REST API — Auto-generated CRUD endpoints for each collection
- Authentication — JWT tokens, OAuth and OIDC sign-in, magic links, one-time codes, MFA, API keys, user/role management
- Storage — File upload/download with local filesystem or S3
- WebSocket — Real-time data sync via PostgreSQL LISTEN/NOTIFY
- Entity History — Audit trail for every data change
- Database Branching — Instant, isolated database copies for dev/staging/testing
- Cron Jobs — Scheduled background tasks with monitoring dashboard
Everything is initialized with a single function:
import { initializeRebaseBackend } from "@rebasepro/server";import { createPostgresAdapter } from "@rebasepro/server-postgres";import { env } from "./env";
const instance = await initializeRebaseBackend({ app, server, collectionsDir: "./config/collections", database: createPostgresAdapter({ connection: db, schema: { tables, enums, relations } }), auth: { jwtSecret: env.JWT_SECRET, }, storage: { type: "local", basePath: "./uploads" }, history: true, enableSwagger: env.NODE_ENV !== "production"});Since 0.25 That one call is
a shortcut over public steps: installUnhandledRejectionHandler keeps a stray
rejection from ending the process, createRebaseBackend builds the backend,
setDefaultBackend makes it the one the module-level API answers for, and
mount, attachRealtime and start serve it. To mount only some route
groups, put them at other paths, leave the timers to another process, or run
two backends in one process, call the steps yourself. See
Custom Server Integration.
Where each option lives
Section titled “Where each option lives”That call is the ejected shape — the one you write yourself after rebase eject, or in a custom server. A scaffolded project does not have it: the
published runtime boots the project, and each option arrives as an environment
variable in .env, an export from config/index.ts, or a directory the bundle
declares in rebase.json.
Both paths reach the same RebaseBackendConfig. This is the whole map.
| Option | Managed runtime |
|---|---|
basePath |
REBASE_BASE_PATH (default /api) |
collections, collectionsDir |
the config/collections/ directory, declared by rebase.json |
functions, functionsDir |
backend/functions/ |
crons, cronsDir |
backend/crons/ |
bootstrappers, database |
DATABASE_URL, plus a database("<key>") declaration in config/resources.ts for every database beyond the default |
auth |
JWT_SECRET, the OAUTH_* variables, and config/collections/users |
storage |
the STORAGE_* variables, plus a bucket("<key>") declaration in config/resources.ts for every bucket beyond the default |
storageAuthorize |
export const storageAuthorize from config/index.ts |
storagePublicRead |
STORAGE_PUBLIC_READ |
storageRenditionCache |
STORAGE_RENDITION_CACHE |
storageInsecureAllowAnyAuthenticated |
STORAGE_ALLOW_ANY_AUTHENTICATED |
callbacks |
export const callbacks from config/index.ts |
history |
REBASE_HISTORY (on by default) — the boolean form only |
enableSwagger |
REBASE_ENABLE_SWAGGER; unset means on outside production |
compression |
REBASE_COMPRESSION |
maxBodySize |
REBASE_MAX_BODY_SIZE |
logging |
LOG_LEVEL |
provisionSchema, surfaces, ownership, functionsSelection, functionsUpstream |
REBASE_ROLE — see Split Processes |
corsHandled |
CORS is installed by the runtime from CORS_ORIGINS |
schemaVersion, runtimeVersion |
the build stamps both into the bundle |
app, server, provisioningDriverResult |
the runtime creates them |
Options with no managed route
Section titled “Options with no managed route”These have no environment variable and no config export. They are reachable
only from a hand-written initializeRebaseBackend call — rebase eject, or a
custom server:
rateLimit · jobs · csrf · cronPersistence · functionsTimeoutMs ·
storagePolicies · storageTriggers · baas · liveSchema · rlsAudit ·
history in its object form ({ retention })
schemaEditor is forced off in a built bundle: the editor rewrites collection
source files, and a bundle holds compiled output.
What Gets Created
Section titled “What Gets Created”After initialization, these routes are mounted:
| Path | Purpose |
|---|---|
/api/auth/* |
Authentication (signup, login, refresh, OAuth, magic links, one-time codes, MFA) |
/api/admin/* |
Users, roles, service keys, cron, backups, logs and schema, each behind its own scope |
/api/storage/* |
File upload, download, and deletion |
/api/data/:slug |
CRUD operations per collection (GET, POST, PATCH, DELETE) |
/api/data/:slug/:id/history |
Entity change history (when enabled) |
/api/docs |
OpenAPI spec (when enableSwagger: true) |
/api/swagger |
Swagger UI (dev mode, when enableSwagger: true) |
/api/meta/contract |
The project’s collection schema (needs schema:read) |
/api/meta/schema-version |
A version string for that schema (unauthenticated) |
/api/functions/* |
Custom function routes (when functions or functionsDir is set) |
/api/admin/cron/* |
Cron job management (cron:read / cron:write, when crons or cronsDir is set) |
| WebSocket on upgrade | Real-time subscriptions, on / for an API at /api |
The Initialization Lifecycle
Section titled “The Initialization Lifecycle”When you invoke initializeRebaseBackend(), the framework triggers a sequential, 5-stage boot sequence:
[Start Boot] │ ▼1. ENV validation (Zod parsing of jwt, databases, cors) │ ▼2. Dynamic Collection Loading (Chokidar watches .ts files, AST parsing) │ ▼3. Database Bootstrapping (Acquires advisory lock, creates schemas/auth/helper SQL functions) │ ▼4. Service Initialization (Auth, Storage S3/Local client instances, Cron store seeding) │ ▼5. Route Mounting & Edge Loading (Hono controllers, custom functions, WebSocket binding) │ ▼[Boot Complete]What happens when boot fails
Section titled “What happens when boot fails”Boot fails loudly. If the database is unreachable, the credentials are
wrong, or the collection schema cannot be applied, initializeRebaseBackend
throws, nothing is served, and the process exits 1. There is no degraded mode
and no partial server: a container that cannot reach its database restarts, and
the error that killed it is the last thing in its logs.
That is deliberate. A server that comes up answering sign-in while every
/api/data/* route fails is far harder to diagnose than one that never comes
up — and an orchestrator can act on a crash loop.
Before the first query, boot probes the connection and prints the diagnosis: the
host and port it could not reach, the driver’s own reason (ECONNREFUSED,
password authentication failed for user "app"), and the fix. See
Troubleshooting for the failure-by-failure list.
Once it is serving: /livez and /health
Section titled “Once it is serving: /livez and /health”Two probes, answering two different questions.
| Path | Touches the database | Answers |
|---|---|---|
/livez |
No | 200 {"status":"ok"} while the process is running. Use it for a liveness probe. |
/health |
Yes, every data source | 200 {"status":"ok"} when every configured data source answers; 503 {"status":"degraded"} when one does not. Use it for a readiness probe. |
A liveness probe on /health is a mistake worth naming: a database blip would
make the orchestrator kill an otherwise healthy process, turning a short outage
into a restart loop.
/health is unauthenticated, so it publishes the verdict and not the reason —
outside development it names which data source is degraded and nothing else. The
driver’s error text quotes the host, port, database name and role, and that goes
to the logs. Both paths are also served under basePath (/api/health).
Configuration Reference
Section titled “Configuration Reference”interface RebaseBackendConfig { // HTTP framework app: Hono; // Hono application instance server: Server; // Node.js HTTP server (for WebSocket attachment) basePath?: string; // Route prefix (default: "/api")
// Collections collections?: CollectionConfig[]; // Your collection definitions collectionsDir?: string; // Auto-load collections from a directory
// Database adapter (PostgreSQL, SQLite, etc.) database?: DatabaseAdapter;
// Authentication configuration or custom adapter auth?: RebaseAuthConfig | AuthAdapter;
// File storage storage?: BackendStorageConfig | Record<string, BackendStorageConfig>;
// Entity history history?: boolean | HistoryConfig;
// OpenAPI/Swagger enableSwagger?: boolean;
// Custom API endpoints: values, or a directory of files (not both) functions?: Record<string, FunctionApp>; // { "send-invoice": app } → /api/functions/send-invoice functionsDir?: string; // Auto-load Hono routes from a directory
// Scheduled tasks: values, or a directory of files (not both) crons?: Record<string, CronJobDefinition>; // { "nightly-report": defineCron(…) } cronsDir?: string; // Auto-load cron jobs from a directory cronPersistence?: boolean; // Write run logs to rebase.cron_logs (default: true)
// HTTP behaviour compression?: boolean; // gzip/deflate for API responses (default: true) maxBodySize?: number; // Request-body ceiling in bytes (default: 10MB; 0 disables) csrf?: { origin: string | string[] | ((origin: string) => boolean) };
// Schema editing schemaEditor?: boolean; // Force the schema-editor routes on or off
// Logging logging?: { level?: "error" | "warn" | "info" | "debug" };}Five of those are easy to miss and change behaviour you can otherwise only observe:
| Key | Default | What it does |
|---|---|---|
compression |
true |
gzip/deflate on API responses, negotiated from Accept-Encoding. Already-compressed, streamed and no-transform bodies are left alone, and so are bodies under 1 KB (gzip costs more than it saves there), so it is safe to leave on — a large JSON list typically drops by ~20x. Set false when nginx, Cloudflare or another proxy in front already compresses, to avoid paying twice. Environment: REBASE_COMPRESSION. |
maxBodySize |
10485760 (10MB) |
Ceiling for request bodies on API routes; 0 disables it. Storage uploads use the storage config’s own maxFileSize (50MB), which wins for those routes. Environment: REBASE_MAX_BODY_SIZE. |
csrf |
off | Opt-in. A BaaS API is called by mobile apps, SPAs on other domains and CLI tools, none of which send an Origin a fixed list would accept — so this is not on by default. Turn it on with the origins your browser clients use. No environment form: eject to set it. |
cronPersistence |
true |
Whether run logs reach rebase.cron_logs. false keeps the jobs running and the history in memory only, which the Studio panel then loses on restart. |
schemaEditor |
on outside production, when collectionsDir is set |
Forces the schema-editor routes on or off. The editor rewrites collection source files, so it needs a directory to write to — and a built bundle has none, which is why a deployment never has it. |
The rest of RebaseBackendConfig is either documented on its own page (auth,
storage, jobs, callbacks, liveSchema, rlsAudit) or marked @internal:
bootstrappers, provisioningDriverResult, provisionSchema, corsHandled,
functionsSelection, functionsUpstream and runtimeVersion are filled in by
bootFromBundle from the environment, and passing them by hand is a way to
disagree with the runtime about what this process is.
The Backend Instance
Section titled “The Backend Instance”initializeRebaseBackend returns the backend it built, already mounted and
started. Its type is RebaseBackendInstance, which is RebaseBackend, the
type createRebaseBackend returns:
const instance = await initializeRebaseBackend(config);
// Internal service accessinstance.driver // Default data driverinstance.driverRegistry // All drivers (for multi-database)instance.realtimeService // Default realtime serviceinstance.auth?.userService // User managementinstance.storageController // Default storageinstance.storageRegistry // All storage backendsinstance.collectionRegistry // Collection metadatainstance.history?.historyService // Entity historyinstance.cronScheduler // Cron job scheduler (when crons or cronsDir is set)instance.jobQueue // Durable job queue (when jobs are enabled)instance.setCollectionCallbacks(slug, callbacks) // Replace a collection's callbacksinstance.shutdown() // Drain and stop; also closes the server it was givenSince 0.25 It also carries
rebase, the in-process client, fetch(request), routes, the route groups
as Hono sub-apps, services, its own JWT keys, access model, API-key store and
rate limiters, and verifyCredential(token); see
Custom Server Integration.
Note: While the
instanceexposes these internal services, application code (such as custom functions and cron jobs) should use therebasesingleton from@rebasepro/serverto interact with the backend API. Since 0.25 Inside a request, cron run or job,rebaseis the backend serving it; elsewhere it is the process default, whichinitializeRebaseBackendsets to its backend.
REST API
Section titled “REST API”The REST API is auto-generated from your collections. Every collection gets these endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/api/data/:slug |
List entities — filtering, sorting, paging and search are query parameters |
GET |
/api/data/:slug/count |
How many rows the same query matches |
GET |
/api/data/:slug/aggregate |
count/sum/avg/min/max, optionally grouped |
GET |
/api/data/:slug/:id |
Get a single entity |
POST |
/api/data/:slug |
Create a new entity |
PATCH |
/api/data/:slug/:id |
Update the fields you send |
DELETE |
/api/data/:slug/:id |
Delete a record |
POST |
/api/data/:slug/bulk |
Create many rows in one transaction |
PATCH |
/api/data/:slug/bulk |
Update many rows in one transaction |
POST |
/api/data/:slug/bulk/delete |
Delete many rows in one transaction |
Query parameters
Section titled “Query parameters”There is one reference for them and it is not this page. REST
API documents both query dialects the server accepts — the
PostgREST-style ?column=op.value form and the JSON ?where= form — along with
orderBy, limit/offset, include, fields, searchString and vector
search. Endpoints is the index of every route the
server mounts, generated ones included.
A parameter the server does not reserve is read as a filter on the column of that name, so an invented one does not fail: it silently matches nothing.
WebSocket
Section titled “WebSocket”The WebSocket server attaches to the same HTTP server and provides real-time subscriptions. Since 0.25 It answers upgrades on one path, / for an API at /api, where the SDK dials, and leaves every other path to your own upgrade listeners; on 0.24 it takes every upgrade on the server:
- Subscribe to collection changes — get notified when any entity in a collection is created, updated, or deleted
- Subscribe to entity changes — get notified when a specific entity changes
- Automatic reconnection handling in the typed SDK
The backend uses PostgreSQL LISTEN/NOTIFY internally. For multi-instance deployments, provide a connectionString in your PostgresBootstrapper to enable cross-instance broadcasting.
Error Handling
Section titled “Error Handling”Every failure — from any route, in any subsystem — comes back in one envelope:
{ "error": { "message": "Entity not found", "code": "NOT_FOUND", "requestId": "9f1c0b8e-4d2a-4e1b-9d0f-2c7a5b3e6a11" }}| Field | Always present | What it is |
|---|---|---|
message |
yes | Written for a person reading a console. Names the obstacle, not the rule. |
code |
yes | SCREAMING_SNAKE_CASE and stable. This is the field to branch on. |
details |
no | Structured payload when the refusal is about something — a list of failing paths, a set of unknown fields. |
requestId |
no | Present when the request carried or was assigned one; echoes X-Request-ID. Quote it in a bug report. |
The HTTP status is on the response, not in the body. Branch on code, not on
message — messages are written for humans and are free to change.
The typed SDK turns every one of these into a RebaseApiError carrying
status, code and details — including the failures that never reached a
server at all. A refused connection, a DNS failure, CORS or an abort arrives as
status: 0, code: "NETWORK_ERROR", with the runtime’s own error on cause,
rather than as whatever fetch felt like rejecting with. A response with no
envelope — a proxy’s 413, a load balancer’s 502 — still gets a code from its
status (PAYLOAD_TOO_LARGE, SERVICE_UNAVAILABLE, INTERNAL_ERROR, …), and a
client built outside a browser with no baseUrl fails with
BASE_URL_REQUIRED rather than a network error.
Since 0.25 A read that gets a success
without the shape it reads — a 204, an empty body, a page with no data
list — fails with UNEXPECTED_RESPONSE instead of resolving as “no rows”, and
createMany, updateMany, deleteMany or batch given something other than
an array fails with INVALID_BULK_BODY, the code the server uses for the same
mistake. So application code catches one class:
async function setPrice(id: string, price: number) { try { return await client.data.products.update(id, { price }); } catch (e) { if (e instanceof RebaseApiError && e.code === "NOT_FOUND") return null; throw e; }}Next Steps
Section titled “Next Steps”- Authentication — JWT, OAuth and OIDC providers, MFA, API keys, user management
- Storage — Local and S3 file storage
- Entity Callbacks — Lifecycle hooks and
context.dataAPI - Entity History — Audit trail
- Custom Functions — Add custom API endpoints
- Cron Jobs — Scheduled background tasks
- Database Branching — Instant database copies for dev/staging
- Deployment — Take the backend to production