Hono · Drizzle · PostgreSQL · OpenAPI

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.

rebase-introspect

Connect to your Postgres database

Point Rebase at your connection string. Everything runs on your infrastructure.

DATABASE_URL
Status: Connected
v3.1.2

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.

die erzeugte Oberfläche
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/42

Der 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.

Die komplette SDK-Tour
src/orders.ts
// 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.

orders · four writers, one streamLISTEN/NOTIFY
  • 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

Four ways to write to the orders table — the REST API, psql, a cron job and Studio's SQL editor — all produce the same change event, which each subscriber receives filtered by 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.

config/collections/orders.tswas du schreibst
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"] },
  ],
};
psql — was die Datenbank durchsetztgenerated
-- 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.
Das ganze Sicherheitsmodell·Schon auf Postgres? 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.

rebase dev — mounted routes
  • /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 TCP
  • WebSocketrow 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.

backend/src/index.tsHono
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
Relationen, in einer Query aufgelöstDrizzle
// 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

config/
collections/users.ts

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
GET /api/data/users
200 OK · application/json
[{ "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.

~pnpm dlx @rebasepro/cli init