Das Backend,
für sich allein.
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.
REST, ohne es zu schreiben
Jede Tabelle, die du freigibst, wird zu einem 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/42Der typisierte Client
Autovervollständigung, die aus deinem Schema kommt
rebase generate-sdk macht aus deinen Collections einen Database-Typ. Gib ihn dem Client, und Collection-Namen, Feldnamen, Filteroperatoren und Rückgabeformen werden zur Compile-Zeit geprüft — samt der Felder, die du in include angefordert hast.
Benenn eine Spalte in der Collection um, und die Aufrufstellen werden rot, bevor irgendetwas ausgeliefert wird. Das Repository, das die API konsumiert, muss nicht das sein, das sie definiert: --from <url> erzeugt denselben Client aus einem laufenden Backend, in einem Projekt ganz ohne eigenes Schema. Web-App, Panel und Mobile-Client bleiben je in ihrem eigenen Repo, und die CI findet einen veralteten Client vor einem Nutzer.
// 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
Änderungen kommen an, egal wie sie gemacht wurden
listen() auf jeder Collection, und jedes Insert, Update und Delete landet über WebSocket beim Client. Subscriptions respektieren dieselbe Row-Level Security wie ein Read — einem Abonnenten wird nie eine Zeile zugestellt, die er nicht auch hätte abrufen können.
Schreibvorgänge außerhalb der API zählen ebenfalls — eine aus psql, einem Cron-Job oder Studios SQL-Editor geänderte Zeile löst dasselbe Event aus, weil die Change-Erfassung in der Datenbank passiert. Broadcast-Kanäle und Presence sind standardmäßig pro Instanz; setz realtime.bus auf { type: "postgres" }, und sie verteilen sich über LISTEN/NOTIFY auf alle Replikate — oder gib ihm einen eigenen Transport.
- RESTPATCH /api/data/orders/42your app
- psqlUPDATE orders SET status = 'shipped' WHERE id = 42;a person at a terminal
- cronUPDATE orders SET status = 'closed' WHERE …a scheduled job
- StudioUPDATE orders … — run from the SQL editorthe admin panel
→ one change event: orders · UPDATE · id 42
→ each subscriber re-reads the row under its own access rules
Authorization
Die Regeln leben in Postgres, nicht in diesem Server
Anfragen laufen als eingeschränkte Datenbankrolle. Deine securityRules kompilieren zu echten Row-Level-Security-Policies — eine Query, die nichts zurückgeben sollte, gibt nichts zurück, egal ob sie über das SDK, einen REST-Aufruf, das Admin-Panel, den API-Key eines Agenten oder eine psql-Sitzung kommt. Eine Collection ohne Policy liefert keine Zeilen: die Voreinstellung ist geschlossen, nicht offen.
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 liest DATABASE_URL und prüft es, ohne dass du etwas installierst.Ein Server
Alles gemountet, nichts zusammengebaut
Das ist die Routing-Tabelle eines Standardprojekts — ein Prozess, ein Deployment, kein Service-Mesh zu zeichnen. Jede davon existiert, weil sie selbst zu bauen eine Woche kostet, die du lieber anders verbringst.
Ein Prozess ist die Voreinstellung, nicht die Decke. Derselbe Build startet auch als API, als Functions-Tier und als Worker — eine Variable pro Prozess entscheidet, welche Routen gemountet werden, welche Timer feuern und wer das Schema besitzt.
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 — schema, rows and policies — with one command to switch your project onto one and back.
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
Langweilige Technik, mit Absicht
Eine Hono-App und ein Drizzle-Schema. Zwei Dinge, die dein Team lesen, debuggen und erweitern kann, ohne ein Framework zu lernen — und ohne darauf zu warten, dass wir einen Hook freigeben.
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- Als Sub-App auf einem Server einhängen, den du schon betreibst
- Deine Middleware, deine Routen, dein Error-Handling
- Node, Docker, Railway, Fly.io oder 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 und many-to-many
- Feldauswahl — hol genau die Spalten, nach denen du gefragt hast
- Cursor- und Offset-Pagination
Die andere Hälfte
Headless als Standard
Das Admin-Panel ist ein Client dieser API, keine Schicht darin. Es liest durch dieselben Policies wie deine App. Schalte es unten ein: Das Projekt bekommt eine Abhängigkeit und einen verschachtelten admin-Block, ein Admin-Panel erscheint — und die API-Antwort rechts bewegt sich nicht.
A headless project: no admin block, no panel — just the API.
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.
Richte es auf deine Datenbank.
Scaffolde gegen ein Postgres, das du schon betreibst, und lies die REST-Routen, das OpenAPI-Dokument und die erzeugten Policies.