Hono · Drizzle · PostgreSQL · React-free

The backend,
on its own.

Point it at a Postgres database and you get REST, a typed SDK, realtime over WebSocket, auth, storage and an OpenAPI spec — with every access rule enforced by the database itself. No admin panel required. No React in the dependency tree.

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

01·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.

api.yourapp.com
the generated surface
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

02·The 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 full SDK tour
app.ts
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,
  });

03·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 frompsql, 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.

04·Authorization

The rules live in Postgres, not in this server

Requests execute as a restricted database role. Your securityRulescompile 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.

config/collections/orders.tswhat you write
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 — what the database enforcesgenerated
-- 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.
The whole security model·Already on Postgres? npx @rebasepro/rls-check $DATABASE_URLaudits it without installing anything.

05·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.

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.

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

06·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.

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
  • 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
relations, resolved in one queryDrizzle
// 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

07·The other half

Headless by default

The admin panel is a separate product. 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

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.

A backend in about a minute

Scaffold a project, point it at a database, and read the OpenAPI spec it generates.

~pnpm dlx @rebasepro/cli init