Hono · Drizzle · PostgreSQL · React-free

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.

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, 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

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

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,
  });

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.

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

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

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

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

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

~pnpm dlx @rebasepro/cli init