The backend,
on its own.
REST, a typed SDK, realtime over WebSocket, auth, storage and an OpenAPI spec — with every access rule enforced by the database itself. Point it at your own Postgres, or run rebase dev and get one without installing anything. No admin panel required. No React in the dependency tree.
REST, without writing it
Every table you expose becomes an endpoint
No controllers, no serializers, no validation middleware. Filtering, sorting, pagination and relation loading are query parameters, in a PostgREST-compatible syntax your team probably already knows.
Nested relations are addressable as paths, so a customer's orders are a URL rather than a join you hand-wrote.
GET /api/data/orders?status=eq.paid&total=gt.100
GET /api/data/orders?or=(status.eq.paid,total.gt.500)
GET /api/data/orders/42?include=customer,items
GET /api/data/customers/7/orders
POST /api/data/orders
PUT /api/data/orders/42
DEL /api/data/orders/42The typed client
Autocomplete that comes from your schema
rebase generate-sdk turns your collections into a Database type. Pass it to the client and collection names, field names, filter operators and return shapes are all checked at compile time — including the fields you asked for in include.
Rename a column in the collection and the call sites turn red before anything ships. The repository that consumes the API does not need to be the one that defines it: --from <url> generates the same client from a running backend, in a project with no schema of its own. Your web app, admin and mobile client each stay in their own repo, and CI catches a stale client before a user does.
// generated once by `rebase generate-sdk`
import type { Database } from "./generated/sdk/database.types";
const client = createRebaseClient<Database>({
baseUrl: "https://api.example.com",
});
// collection names, field names and return types all autocomplete
const { data } = await client.data.orders
.find({
filter: { status: ["eq", "paid"] },
include: ["customer"],
limit: 50,
});Realtime
Changes arrive, however they were made
listen() on any collection and every insert, update and delete lands on the client over WebSocket. Subscriptions respect the same row-level security as a read, so a subscriber is never pushed a row they could not have fetched.
Writes made outside the API count too — a row changed from psql, a cron job or Studio's SQL editor emits the same event, because change capture happens in the database. Across replicas, fan-out rides Postgres LISTEN/NOTIFY.
Authorization
The rules live in Postgres, not in this server
Requests execute as a restricted database role. Your securityRules compile to real row-level security policies, so a query that should return nothing returns nothing — whether it arrives through the SDK, a REST call, the admin panel, an agent's API key or a psql session. A collection with no policy serves no rows: the default is closed, not open.
export const orders = {
slug: "orders",
table: "orders",
securityRules: [
// customers see their own orders…
{ operation: "select", ownerField: "customer_id" },
// …support can read and update all of them
{ operations: ["select", "update"],
roles: ["support"] },
],
};-- rebase schema generate → db push
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_select_1f4c9ab ON orders
FOR SELECT TO rebase_user
USING (customer_id = rebase.uid());
CREATE POLICY orders_update_8b02e13 ON orders
FOR UPDATE TO rebase_user
USING (string_to_array(rebase.roles(), ',')
&& ARRAY['support']);
-- no policy for INSERT or DELETE:
-- nobody can insert or delete. Closed by default.npx @rebasepro/rls-check $DATABASE_URL audits it without installing anything.One server
Everything mounted, nothing assembled
This is the routing table of a default project — one process, one deployment, no service mesh to draw. Each of these exists because building it yourself is a week you would rather spend elsewhere.
One process is the default, not the ceiling. The same build also boots as an API, a functions tier and a worker — one variable per process decides which routes mount, which timers fire and which one owns the schema.
Topics and jobs. Publish an event, subscribe by name. One durable row per subscriber, each retrying on its own schedule — and a publish inside a transaction that rolls back never happened.
Webhooks. HMAC-signed outbound calls on insert, update and delete, with retries.
Entity history. An audit trail per collection: who changed what, when, and a revert.
Database branching. Isolated copies of the whole database for a feature or a test run.
Email. SMTP for verification, password reset and your own transactional templates.
Backups. Role-complete dumps and restores, driven from the CLI or the admin API.
Multiple sources. Route different collections to different databases from one server.
/api/data/:collectionCRUD, filters, sorting, pagination, relation includes, nested subcollections/api/auth/*sign-up, login, refresh, password reset, OAuth, MFA, roles/api/storage/*upload, download, signed access — local, S3-compatible or GCS/api/functions/:nameyour own Hono routes, auto-mounted from functions//api/cron/*scheduled jobs with run history and manual triggers/api/docs · /api/swaggerOpenAPI 3.0 spec, plus an explorer in dev/api/metathe runtime contract: collections, properties, capabilities/healthreadiness for your orchestrator — asserts the auth schema, not just TCPWebSocketrow subscriptions, broadcast channels and presence on the same server
Underneath
Boring technology, on purpose
A Hono app and a Drizzle schema. Two things your team can read, debug and extend without learning a framework — and without waiting for us to expose a hook.
import { initializeRebaseBackend } from "@rebasepro/server";
import { createPostgresBootstrapper } from "@rebasepro/server-postgres";
const backend = await initializeRebaseBackend({
server,
app,
bootstrappers: [
createPostgresBootstrapper({
connection: db,
schema: { tables, enums, relations },
})
],
auth: { jwtSecret: process.env.JWT_SECRET },
storage: { type: "local", basePath: "./uploads" },
});
// it's a Hono app — mount it, wrap it, add your own middleware- Mount it as a sub-app on a server you already run
- Your middleware, your routes, your error handling
- Node, Docker, Railway, Fly.io or bare metal
// one collection, one relation
const posts = {
slug: "posts",
properties: { title: { type: "string" } },
relations: [{
relationName: "author",
target: () => users,
cardinality: "one-to-one"
}]
};
// GET /api/data/posts?include=author
// → one query via db.query.findMany, not 1 + N- One-to-one, one-to-many and many-to-many
- Field selection — fetch the columns you asked for
- Cursor and offset pagination
The other half
Headless by default
The admin panel is a client of this API, not a layer inside it. It reads through the same policies your app does. Toggle it on below: the project gains one dependency and a nested admin block, a back office appears — and the API response on the right does not move.
A headless project: no React, no admin block, no panel.
Your project
Dependencies
- @rebasepro/server
- @rebasepro/server-postgres
- @rebasepro/client
Without admin.d.ts, an `admin` key on a collection is a type error.
config/collections/users.ts
export const users = {
name: "Users",
table: "users",
properties: {
email: { type: "string" },
displayName: { type: "string" },
},
securityRules: [
{ operation: "select",
using: "id = rebase.uid()::uuid" },
], admin: {
icon: "Users",
group: "Settings",
listProperties: ["displayName", "email"],
},};
The server loads this file either way — and never reads inside admin.
Your API
identical[{ "id": "9f2…", "email": "ada@…",
"displayName": "Ada" }]Admin panel
Not installed.
Nothing is served, nothing is bundled.
The only thing that changed is what a human can see.
A backend in about a minute
Scaffold a project and read the OpenAPI spec it generates. A database comes up with it, so there is nothing to install first — and naming your own DATABASE_URL overrides it at any point.
