# Rebase Documentation > Rebase is an open-source Postgres admin panel and headless CMS built with React and TypeScript. > Connect your existing PostgreSQL database and get a full admin UI, REST & GraphQL APIs, a SQL editor, > row-level security, and an MCP server for AI agents — all generated from your schema. > Rebase uses Drizzle ORM under the hood and keeps your TypeScript collection definitions as the single source of truth. > Great for existing projects (it adapts to any Postgres schema) and new ones alike. ## Introduction ### What is Rebase? Rebase is an **open-source Backend-as-a-Service (BaaS)** and **admin panel** built with React and TypeScript. It gives you everything you need to build production-ready back-office applications on top of **PostgreSQL**: - **Schema as Code** — Define your data models as TypeScript collections. Rebase generates PostgreSQL tables, a full CRUD UI, REST API endpoints, and type-safe client SDKs from a single source of truth. - **Instant Admin Panel** — A beautiful, fast React-based admin UI with spreadsheet tables, forms, Kanban boards, inline editing, and real-time sync. - **Integrated Backend** — Authentication (JWT + Google OAuth), file storage (local or S3), Row Level Security, entity history, and WebSocket real-time — all built in. - **Radically Extensible** — If you can build it in React, you can add it to Rebase. Custom fields, custom views, plugins, and full access to internal hooks. :::tip Rebase is **not** a hosted SaaS. You self-host it, you own your data, and you own your code. Think of it as an open-source alternative to Supabase or Retool that you control end-to-end. ::: ### How It Works ``` ┌──────────────────────────────────────────────────────┐ │ Client Applications │ │ ┌──────────────┐ ┌───────────────────────────────┐ │ │ │ Your App │ │ Rebase Admin UI │ │ │ │ (Any Tech) │ │ Tables / Forms / Kanban │ │ │ └──────┬───────┘ └──────────────┬────────────────┘ │ └─────────┼──────────────────────────┼─────────────────┘ │ HTTP + WebSocket │ ▼ ▼ ┌──────────────────────────────────────────────────────┐ │ Rebase Backend (Node.js + Hono) │ │ ┌─────────┐ ┌──────┐ ┌─────────┐ ┌───────────────┐ │ │ │ REST API│ │ Auth │ │ Storage │ │ WebSocket RT │ │ │ └────┬────┘ └──┬───┘ └────┬────┘ └──────┬────────┘ │ └───────┼─────────┼──────────┼─────────────┼───────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────────────────────────────────────────────┐ │ PostgreSQL (via Drizzle ORM) │ └──────────────────────────────────────────────────────┘ ``` ### Quick Start ```bash npx create-rebase-app my-app cd my-app ``` Configure your database in `.env`: ```bash DATABASE_URL=postgresql://user:password@localhost:5432/mydb JWT_SECRET=your-secret-key ``` Start everything: ```bash pnpm dev ``` That's it. Your admin panel is running at `http://localhost:5173` and the API at `http://localhost:3001`. → Follow the full [Quickstart guide](/docs/getting-started/quickstart) for a complete walkthrough. ### Core Features | Feature | Description | |---------|-------------| | **Spreadsheet Views** | Fast, virtualized table with inline editing, filtering, sorting, and text search | | **Kanban Board** | Drag-and-drop board view grouped by any enum property | | **Relations** | One-to-one, one-to-many, many-to-many with junction tables and multi-hop joins | | **Row Level Security** | Supabase-style RLS policies defined in your collection config | | **Authentication** | JWT + refresh tokens, Google OAuth, role-based access control | | **File Storage** | Local filesystem or S3-compatible with upload fields and browser | | **Real-time Sync** | WebSocket-powered live updates across all connected clients | | **Entity History** | Full audit trail for every create, update, and delete | | **REST API** | Auto-generated CRUD endpoints for every collection | | **Data Import/Export** | CSV, JSON, and Excel import with field mapping; CSV/JSON export | | **Collection Editor** | Visual schema editor that generates TypeScript via AST manipulation | | **Client SDK** | Type-safe JavaScript SDK for your frontend apps | | **Plugin System** | Extend every part of the UI — toolbar, forms, fields, home page | | **AI Data Enhancement** | LLM-powered autocompletion for text fields | ### Next Steps - **[Quickstart](/docs/getting-started/quickstart)** — Get running in 2 minutes - **[Project Structure](/docs/getting-started/project-structure)** — Understand the generated code - **[Collections](/docs/collections)** — Define your data schema - **[Backend](/docs/backend)** — Configure auth, storage, and the API - **[Architecture](/docs/architecture)** — Understand how it all fits together ## Quickstart ### Create a New Project ```bash npx create-rebase-app my-app ``` This scaffolds a project with three packages: | Folder | Description | |--------|-------------| | `frontend/` | React SPA — Vite + TypeScript with the Rebase admin UI | | `backend/` | Node.js server — Hono, PostgreSQL via Drizzle ORM, WebSocket | | `shared/` | TypeScript collection definitions shared by both sides | ### Prerequisites - **Node.js** 18+ - **PostgreSQL** — local install, Docker, or any managed Postgres (Neon, Supabase, RDS) - **pnpm** (recommended) or npm ### Configure Your Environment After scaffolding, edit the `.env` file at the project root: ```bash ## Database connection string DATABASE_URL=postgresql://username:password@localhost:5432/your_database ## JWT secret for authentication (generate a strong random string) JWT_SECRET=change-me-to-a-random-secret ## Frontend URL for CORS VITE_API_URL=http://localhost:3001 ## Optional: Google OAuth client ID ## VITE_GOOGLE_CLIENT_ID=your-google-client-id ``` ### Start the Dev Servers ```bash pnpm dev ``` This starts: - **Backend** at `http://localhost:3001` — REST API, auth, storage, WebSocket - **Frontend** at `http://localhost:5173` — Rebase admin panel - **Hot reload** for both — changes take effect instantly You can also start them individually: ```bash pnpm dev:backend ## Backend only pnpm dev:frontend ## Frontend only ``` ### First Login When you open `http://localhost:5173`, you'll see the login screen. The **first user** to register automatically becomes an admin — this is the bootstrap flow. 1. Click **Sign Up** 2. Enter your email and password 3. You're in — with full admin access ### Define Your First Collection Open `shared/collections/` and create a new file: ```typescript title="shared/collections/products.ts" export const productsCollection: EntityCollection = { slug: "products", name: "Products", singularName: "Product", dbPath: "products", properties: { name: { type: "string", name: "Name", validation: { required: true } }, price: { type: "number", name: "Price", validation: { required: true, min: 0 } }, description: { type: "string", name: "Description", multiline: true }, active: { type: "boolean", name: "Active", defaultValue: true }, created_at: { type: "date", name: "Created At", autoValue: "on_create" } } }; ``` ### Generate the Database Schema ```bash rebase schema generate ## Generate Drizzle schema from your collections rebase db push ## Push the schema to your database ``` Restart the dev servers and your new **Products** collection appears in the navigation. ### Database Commands Reference | Command | Description | |---------|-------------| | `rebase schema generate` | Generate Drizzle schema from your TypeScript collections | | `rebase db push` | Push schema changes directly to the database (dev only) | | `rebase db generate` | Generate SQL migration files | | `rebase db migrate` | Run pending migrations | ### What's Next - **[Project Structure](/docs/getting-started/project-structure)** — Understand the generated code - **[Collections](/docs/collections)** — Deep dive into schema definition - **[Environment & Configuration](/docs/getting-started/configuration)** — All configuration options - **[Deployment](/docs/getting-started/deployment)** — Deploy to production ## Project Structure A Rebase project generated by `npx create-rebase-app` has three interconnected packages: ``` my-app/ ├── .env ## Environment variables (DATABASE_URL, JWT_SECRET, etc.) ├── package.json ## Root workspace config │ ├── frontend/ ## React admin panel (Vite) │ ├── src/ │ │ ├── App.tsx ## Main application component │ │ ├── main.tsx ## React entry point │ │ └── index.css ## Global styles │ ├── package.json │ └── vite.config.ts │ ├── backend/ ## Node.js API server (Hono) │ ├── src/ │ │ ├── index.ts ## Server entry — initializes Rebase backend │ │ └── schema.generated.ts ## Auto-generated Drizzle schema │ ├── drizzle.config.ts ## Drizzle ORM configuration │ ├── Dockerfile │ └── package.json │ └── shared/ ## Collection definitions └── collections/ ├── index.ts ## Exports all collections └── products.ts ## Example: products collection ``` ### Frontend (`frontend/`) The frontend is a standard **Vite + React + TypeScript** application. The key file is `App.tsx`, which wires together all Rebase controllers: ```typescript title="frontend/src/App.tsx" // The client connects to your backend API and WebSocket const rebaseClient = createRebaseClient({ baseUrl: "http://localhost:3001", websocketUrl: "ws://localhost:3001" }); // Collections are imported via a Vite virtual module // that reads from the shared/ directory ``` #### Key Concepts - **`createRebaseClient`** — Creates the SDK client that handles HTTP requests, WebSocket connections, and auth token management - **`virtual:rebase-collections`** — A Vite plugin that auto-imports your shared collections at build time - **Controllers** — `useBuildNavigationStateController`, `useBuildCollectionRegistryController`, etc. — these configure routing, collection resolution, and UI configuration ### Backend (`backend/`) The backend is a **Node.js server** built on [Hono](https://hono.dev/) (a fast, lightweight HTTP framework). The entry point `index.ts` initializes everything: ```typescript title="backend/src/index.ts" const app = new Hono(); await initializeRebaseBackend({ app, server, collections, driver: { connection: db, schema: { tables, enums, relations } }, auth: { jwtSecret: process.env.JWT_SECRET!, google: { clientId: process.env.GOOGLE_CLIENT_ID }, }, storage: { type: "local", basePath: "./uploads" }, history: true }); ``` `initializeRebaseBackend` sets up: - **REST API** routes at `/api/data/*` — auto-generated CRUD for each collection - **Auth** routes at `/api/auth/*` — signup, login, refresh, Google OAuth - **Storage** routes at `/api/storage/*` — file upload/download - **WebSocket** server — real-time entity sync via Postgres LISTEN/NOTIFY - **History** — audit trail recording on every entity change ### Shared Collections (`shared/`) Collections are the **single source of truth** for your data model. They are defined as TypeScript and consumed by both the frontend (for UI generation) and the backend (for schema generation and API routing). ```typescript title="shared/collections/products.ts" export const productsCollection: EntityCollection = { slug: "products", name: "Products", dbPath: "products", properties: { name: { type: "string", name: "Name" }, price: { type: "number", name: "Price" } } }; ``` The `slug` becomes the URL path in the admin UI and the REST API endpoint (`/api/data/products`). The `dbPath` maps to the PostgreSQL table name. ### How They Connect 1. **You define** collections in `shared/` 2. **The backend** reads them to generate Drizzle schemas and mount REST routes 3. **The frontend** reads them (via Vite plugin) to render tables, forms, and navigation 4. **The CLI** reads them to generate migration files with `rebase schema generate` Changes to collections propagate everywhere automatically. ### Next Steps - **[Template Walkthrough](/docs/getting-started/template-walkthrough)** — Line-by-line explanation of the generated code - **[Configuration](/docs/getting-started/configuration)** — All environment variables and options ## Environment & Configuration ### Environment Variables All configuration is done via environment variables in your `.env` file at the project root. #### Required | Variable | Description | Example | |----------|-------------|---------| | `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/mydb` | | `JWT_SECRET` | Secret key for signing JWT tokens. Use a strong random string (min 32 chars). | `a1b2c3d4e5...` | #### Frontend | Variable | Description | Default | |----------|-------------|---------| | `VITE_API_URL` | Backend API URL. Used by the client SDK. | `http://localhost:3001` | | `VITE_GOOGLE_CLIENT_ID` | Google OAuth client ID. Enables "Sign in with Google". | — | #### Backend | Variable | Description | Default | |----------|-------------|---------| | `PORT` | Port for the backend HTTP server | `3001` | | `LOG_LEVEL` | Logging verbosity: `error`, `warn`, `info`, `debug` | `info` | | `NODE_ENV` | Environment: `development` or `production` | `development` | #### Authentication | Variable | Description | Default | |----------|-------------|---------| | `JWT_SECRET` | Secret for JWT signing (required if auth is enabled) | — | | `ACCESS_TOKEN_EXPIRES_IN` | Access token lifetime | `1h` | | `REFRESH_TOKEN_EXPIRES_IN` | Refresh token lifetime | `30d` | | `ALLOW_REGISTRATION` | Allow new users to register (`true`/`false`). First user can always register. | `false` | | `GOOGLE_CLIENT_ID` | Google OAuth client ID (backend validation) | — | #### Storage | Variable | Description | Default | |----------|-------------|---------| | `STORAGE_TYPE` | Storage backend: `local` or `s3` | `local` | | `STORAGE_BASE_PATH` | Base path for local storage | `./uploads` | | `S3_BUCKET` | S3 bucket name (when `STORAGE_TYPE=s3`) | — | | `S3_REGION` | AWS region | — | | `S3_ACCESS_KEY_ID` | AWS access key | — | | `S3_SECRET_ACCESS_KEY` | AWS secret key | — | | `S3_ENDPOINT` | Custom S3 endpoint (for MinIO, Cloudflare R2, etc.) | — | #### Email (Optional) | Variable | Description | |----------|-------------| | `SMTP_HOST` | SMTP server host | | `SMTP_PORT` | SMTP server port | | `SMTP_USER` | SMTP username | | `SMTP_PASS` | SMTP password | | `EMAIL_FROM` | Sender address for system emails | ### Backend Config Object The `RebaseBackendConfig` passed to `initializeRebaseBackend()` provides programmatic control: ```typescript await initializeRebaseBackend({ app, server, collections, basePath: "/api", // Base path for all API routes (default: "/api") driver: { // PostgreSQL driver config connection: db, schema: { tables, enums, relations } }, auth: { // Authentication config jwtSecret: process.env.JWT_SECRET!, accessExpiresIn: "1h", refreshExpiresIn: "30d", requireAuth: true, // Require auth for data API (default: true) allowRegistration: false, google: { clientId: process.env.GOOGLE_CLIENT_ID } }, storage: { // File storage config type: "local", basePath: "./uploads" }, history: true, // Enable entity change history enableSwagger: true, // Enable OpenAPI docs at /api/data/docs logging: { level: "info" } }); ``` ### Next Steps - **[Deployment](/docs/getting-started/deployment)** — Production deployment guide - **[Backend Overview](/docs/backend)** — Full backend configuration reference ## Deployment ### Docker Compose (Recommended) The generated project includes a `Dockerfile` and `docker-compose.yml`. This is the simplest way to deploy: ```yaml title="docker-compose.yml" services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: rebase POSTGRES_PASSWORD: rebase POSTGRES_DB: rebase volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432" app: build: ./backend ports: - "3001:3001" environment: DATABASE_URL: postgresql://rebase:rebase@postgres:5432/rebase JWT_SECRET: ${JWT_SECRET} NODE_ENV: production depends_on: - postgres volumes: - uploads:/app/uploads volumes: pgdata: uploads: ``` ```bash docker compose up -d ``` ### Production Checklist Before deploying to production, ensure: | Item | Details | |------|---------| | **JWT_SECRET** | Use a cryptographically strong random string (≥ 32 chars). Never reuse across environments. | | **DATABASE_URL** | Use a managed Postgres instance (Neon, Supabase, RDS) with TLS enabled | | **CORS** | Configure allowed origins on your backend if frontend and backend are on different domains | | **Storage volumes** | Mount persistent volumes for file uploads. Or switch to S3 for production. | | **HTTPS** | Terminate TLS at your reverse proxy (nginx, Cloudflare, load balancer) | | **Registration** | Set `ALLOW_REGISTRATION=false` after creating your admin account | ### Serving the Frontend In production, the backend can serve the frontend as a static SPA: ```typescript // After initializeRebaseBackend() serveSPA(app, "./frontend/dist"); ``` Build the frontend first: ```bash cd frontend && pnpm build ``` This way you only need to deploy one server that handles both SPA and API. ### Cloud Platforms #### Railway / Render / Fly.io 1. Push your code to a Git repository 2. Connect the repo to your cloud platform 3. Set environment variables (`DATABASE_URL`, `JWT_SECRET`, etc.) 4. The included `Dockerfile` will be auto-detected #### Google Cloud Run ```bash ## Build the container docker build -t gcr.io/YOUR_PROJECT/rebase-backend ./backend ## Push to Container Registry docker push gcr.io/YOUR_PROJECT/rebase-backend ## Deploy gcloud run deploy rebase-backend \ --image gcr.io/YOUR_PROJECT/rebase-backend \ --set-env-vars DATABASE_URL=...,JWT_SECRET=... \ --allow-unauthenticated ``` :::caution Cloud Run instances are stateless. Use **S3 storage** instead of local filesystem for file uploads, and enable **cross-instance realtime** by providing a `connectionString` in your driver config so WebSocket updates propagate across replicas. ::: ### Changing the Base URL If you want Rebase to run at a sub-path (e.g., `/admin`): **Frontend** — Update the `BrowserRouter` basename: ```tsx title="frontend/src/main.tsx" ``` **Backend** — Update the base path: ```typescript await initializeRebaseBackend({ // ... basePath: "/admin/api" }); ``` ### Next Steps - **[Backend Overview](/docs/backend)** — Full backend configuration - **[Storage Configuration](/docs/storage/configuration)** — S3 setup for production ## Architecture Overview ### System Architecture Rebase is a full-stack platform with four layers: ``` ┌─────────────────────────────────────────────────────────────────┐ │ Frontend Layer │ │ React Admin UI • Custom Views • Plugins • Your App │ │ @rebasepro/core • @rebasepro/ui • @rebasepro/studio │ └───────────────────────────┬─────────────────────────────────────┘ │ HTTP + WebSocket ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Backend Layer │ │ Hono HTTP Server • REST API • Auth • Storage • WS │ │ @rebasepro/backend │ └───────────────────────────┬─────────────────────────────────────┘ │ Drizzle ORM ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Database Layer │ │ PostgreSQL • Tables • RLS Policies • LISTEN/NOTIFY │ └─────────────────────────────────────────────────────────────────┘ ``` ### Key Components #### Driver Registry The backend supports multiple database drivers simultaneously. Each driver handles CRUD operations, search, and real-time notifications for its connected database. ```typescript // Single driver (most common) driver: { connection: db, schema: { tables, enums, relations } } // Multiple drivers driver: { "(default)": postgresConfig, "analytics": analyticsDbConfig } ``` Collections specify which driver they use via the `driver` property. Collections without a `driver` use `"(default)"`. #### Collection Registry The `BackendCollectionRegistry` is the runtime index of all collections, their database tables, enums, and Drizzle relations. It's populated at startup from your collection definitions. #### Realtime Service Real-time sync uses PostgreSQL's native `LISTEN/NOTIFY` mechanism: 1. A data mutation happens (insert, update, delete) 2. The backend emits a `NOTIFY` on a Postgres channel 3. The `RealtimeService` receives the notification 4. It broadcasts the change to all connected WebSocket clients 5. React components re-render with the new data For **multi-instance deployments** (e.g., Cloud Run with multiple replicas), provide a `connectionString` in your driver config. This creates a dedicated Postgres connection for cross-instance broadcasting. #### Storage Registry Like drivers, storage backends are registered in a registry. You can have multiple storage providers (local, S3) and route different file fields to different backends using `storageId`. ### Package Map | Package | Role | Used By | |---------|------|---------| | `@rebasepro/types` | TypeScript interfaces for collections, properties, entities, plugins | Everything | | `@rebasepro/backend` | Backend server initialization, REST API, auth, storage, WebSocket | Backend | | `@rebasepro/client` | Client SDK — HTTP transport, WebSocket, auth | Frontend | | `@rebasepro/core` | React framework — Scaffold, controllers, forms, routes, hooks | Frontend | | `@rebasepro/ui` | Standalone UI component library (Tailwind v4 + Radix) | Frontend | | `@rebasepro/auth` | Login views, auth controller hooks, user management | Frontend | | `@rebasepro/studio` | Collection editor, SQL console, JS console, RLS editor, storage browser | Frontend | | `@rebasepro/cli` | CLI for schema generation, DB migrations, SDK generation | Dev tooling | | `@rebasepro/formex` | Lightweight React form state management | Frontend | | `@rebasepro/data_enhancement` | AI-powered field autocompletion plugin | Frontend | | `@rebasepro/data_import_export` | CSV/JSON/Excel import and export | Frontend | | `@rebasepro/schema_inference` | Auto-detect schema from existing database data | Backend/CLI | ### Data Flow #### Read Flow 1. User opens a collection in the admin UI 2. Client SDK sends `GET /api/data/:slug` + opens a WebSocket subscription 3. Backend queries PostgreSQL via Drizzle ORM 4. Data transformer deserializes database rows into entity format 5. Response sent to frontend, components render 6. WebSocket keeps the view synced in real-time #### Write Flow 1. User edits an entity in the form 2. `beforeSave` callbacks run (validation, transformation) 3. Client SDK sends `PUT /api/data/:slug/:id` 4. Backend serializes values, runs Drizzle `UPDATE` 5. `afterSave` callbacks run (side effects) 6. `NOTIFY` broadcast triggers WebSocket update to all clients 7. If history is enabled, a snapshot is recorded ### Next Steps - **[Schema as Code](/docs/architecture/schema-as-code)** — The TypeScript-first approach - **[Backend Overview](/docs/backend)** — Server configuration - **[Collections](/docs/collections)** — Define your data schema ## Schema as Code ### The Core Idea In Rebase, your **TypeScript collection definitions are the single source of truth**. From one set of TypeScript objects, Rebase generates: - **PostgreSQL tables** via Drizzle ORM schema generation - **CRUD UI** — forms, tables, validation, field types - **REST API** endpoints with filtering, sorting, and pagination - **Client SDK** — type-safe data operations - **RLS policies** — Row Level Security in Postgres This means your schema is: - **Version controlled** — every change is a git commit - **Type-safe** — TypeScript catches errors at compile time - **Reviewable** — schema changes go through pull requests - **Portable** — the same definition works across frontend, backend, and CLI ### Visual Editing with AST Manipulation Rebase also provides a **visual collection editor** in Studio mode. When a non-developer uses the visual editor to add a field: 1. The Studio does **not** directly modify the database 2. Instead, it uses [ts-morph](https://ts-morph.com/) to parse your TypeScript source file as an AST 3. It inserts the new property definition precisely into the `properties` block 4. **All existing code, callbacks, and custom logic are preserved untouched** 5. The file is saved, triggering hot reload This "UI as Code Generator" approach means visual edits produce the same clean TypeScript a developer would write by hand. ### Schema Generation Pipeline ``` TypeScript Collections │ ▼ rebase schema generate │ ▼ Drizzle Schema (schema.generated.ts) │ ▼ rebase db generate │ ▼ SQL Migration Files │ ▼ rebase db migrate │ ▼ PostgreSQL Tables ``` #### Example Given this collection: ```typescript const productsCollection: EntityCollection = { slug: "products", dbPath: "products", properties: { name: { type: "string", name: "Name", validation: { required: true } }, price: { type: "number", name: "Price", columnType: "numeric" }, active: { type: "boolean", name: "Active", defaultValue: true }, created_at: { type: "date", name: "Created", autoValue: "on_create" } } }; ``` Rebase generates this Drizzle schema: ```typescript // schema.generated.ts export const products = pgTable("products", { id: serial("id").primaryKey(), name: varchar("name").notNull(), price: numeric("price"), active: boolean("active").default(true), created_at: timestamp("created_at").defaultNow() }); ``` Which produces this SQL: ```sql CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, price NUMERIC, active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() ); ``` ### Next Steps - **[Collections](/docs/collections)** — Full collection configuration reference - **[Drizzle Schema Generation](/docs/backend/schema-generation)** — Detailed column type mappings ## Collections ### What is a Collection? A **collection** is a TypeScript object that describes a database table and how it should appear in the admin UI. It defines: - **Schema** — Properties (columns), their types, and validation rules - **Relations** — Foreign keys, junction tables, and join paths - **Security** — Row Level Security policies - **UI behavior** — View modes, inline editing, entity views, actions - **Lifecycle hooks** — Callbacks for create, update, delete operations ```typescript export const productsCollection: EntityCollection = { slug: "products", // URL path and API endpoint name: "Products", // Display name (plural) singularName: "Product", // Display name (singular) dbPath: "products", // PostgreSQL table name icon: "inventory_2", // Material icon key properties: { name: { type: "string", name: "Product Name", validation: { required: true } }, price: { type: "number", name: "Price", validation: { required: true, min: 0 } }, category: { type: "string", name: "Category", enum: [ { id: "electronics", label: "Electronics", color: "blueDark" }, { id: "clothing", label: "Clothing", color: "pinkLight" }, { id: "books", label: "Books", color: "orangeDark" } ] }, description: { type: "string", name: "Description", multiline: true }, active: { type: "boolean", name: "Active", defaultValue: true }, created_at: { type: "date", name: "Created At", autoValue: "on_create", readOnly: true } } }; ``` ### Key Properties #### Identification | Property | Type | Description | |----------|------|-------------| | `slug` | `string` | **Required.** URL-safe identifier. Used in the admin UI URL and REST API path (`/api/data/{slug}`). | | `name` | `string` | **Required.** Display name (plural). Shown in navigation and page headers. | | `singularName` | `string` | Display name for a single entity. Used in "New Product", "Edit Product", etc. | | `dbPath` | `string` | **Required.** PostgreSQL table name. If different from `slug`, allows you to decouple URLs from table names. | | `icon` | `string` | Material icon key. See [Google Fonts Icons](https://fonts.google.com/icons). | #### Schema | Property | Type | Description | |----------|------|-------------| | `properties` | `Properties` | **Required.** Map of property key → property definition. Each key becomes a database column. | | `relations` | `Relation[]` | SQL relations — foreign keys, junction tables. See [Relations](/docs/collections/relations). | | `securityRules` | `SecurityRule[]` | Row Level Security policies. See [Security Rules](/docs/collections/security-rules). | #### UI Configuration | Property | Type | Default | Description | |----------|------|---------|-------------| | `defaultViewMode` | `"table" \| "cards" \| "kanban"` | `"table"` | Default view mode | | `enabledViews` | `ViewMode[]` | All three | Which view modes are available | | `kanban` | `KanbanConfig` | — | Kanban configuration (column property) | | `openEntityMode` | `"side_panel" \| "full_screen"` | `"full_screen"` | How entities open for editing | | `inlineEditing` | `boolean` | `true` | Enable inline editing in the spreadsheet view | | `defaultSize` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Default row height in the table | | `pagination` | `boolean \| number` | `true` (50) | Enable pagination and/or set page size | | `propertiesOrder` | `string[]` | — | Column order in the table view | | `hideFromNavigation` | `boolean` | `false` | Hide from the sidebar navigation | #### Entity Options | Property | Type | Default | Description | |----------|------|---------|-------------| | `formAutoSave` | `boolean` | `false` | Auto-save on field change | | `hideIdFromForm` | `boolean` | `false` | Hide the entity ID from the form | | `hideIdFromCollection` | `boolean` | `false` | Hide the ID column from the table | | `includeJsonView` | `boolean` | `false` | Show a JSON tab in the entity view | | `history` | `boolean` | `false` | Track changes in entity history | | `alwaysApplyDefaultValues` | `boolean` | `false` | Apply default values on every save, not just creation | #### Advanced | Property | Type | Description | |----------|------|-------------| | `callbacks` | `EntityCallbacks` | Lifecycle hooks (`beforeSave`, `afterSave`, `beforeDelete`, etc.) | | `entityActions` | `EntityAction[]` | Custom actions on entities (archive, publish, etc.) | | `Actions` | `React.ComponentType` | Custom toolbar actions component | | `entityViews` | `EntityCustomView[]` | Custom tabs in the entity detail view | | `additionalFields` | `AdditionalFieldDelegate[]` | Computed/virtual columns | | `subcollections` | `() => EntityCollection[]` | Nested collections (e.g., order → line items) | | `exportable` | `boolean \| ExportConfig` | Enable data export | | `driver` | `string` | Database driver to use (default: `"(default)"`) | ### Collection Builder For dynamic collections that change based on the user or external data, use a builder function: ```typescript const collectionsBuilder: EntityCollectionsBuilder = ({ user, authController }) => { const collections = [productsCollection]; if (authController.extra?.role === "admin") { collections.push(adminSettingsCollection); } return collections; }; ``` ### Filtering and Sorting You can set default or forced filters: ```typescript { // Default filter — users can change it filter: { active: ["==", true] }, // Forced filter — cannot be changed forceFilter: { tenant_id: ["==", currentTenantId] }, // Default sort sort: ["created_at", "desc"] } ``` ### Next Steps - **[Properties](/docs/collections/properties)** — All property types and options - **[Relations](/docs/collections/relations)** — Foreign keys, junction tables, joins - **[Security Rules](/docs/collections/security-rules)** — Row Level Security - **[View Modes](/docs/collections/view-modes)** — Table, Cards, Kanban - **[Entity Callbacks](/docs/collections/callbacks)** — Lifecycle hooks ## Properties ### Overview Properties define the columns in your database table and how they are rendered in the admin UI. Each property has a `type` that determines: - The **database column type** (via Drizzle schema generation) - The **form field** component - The **table cell** renderer - The **validation** rules ### Property Types | Type | Description | PostgreSQL Column | |------|-------------|-------------------| | `string` | Text, select, markdown, file upload, URL, email | `varchar`, `text`, `jsonb` | | `number` | Integer, decimal, currency | `integer`, `numeric`, `bigint`, `serial` | | `boolean` | True/false toggle | `boolean` | | `date` | Date, datetime, timestamp | `timestamp`, `date` | | `array` | Ordered list of values | `jsonb` | | `map` | Key-value object | `jsonb` | | `geopoint` | Latitude/longitude pair | `jsonb` | | `reference` | Embedded reference to another entity | `varchar` (stores ID) | | `relation` | SQL foreign key relation | Uses the `relations` array | ### Common Properties All property types share these options: | Property | Type | Description | |----------|------|-------------| | `type` | `string` | **Required.** Data type (see above) | | `name` | `string` | **Required.** Display label | | `description` | `string` | Help text shown below the field | | `columnWidth` | `number` | Column width in pixels (table view) | | `readOnly` | `boolean` | Prevent editing | | `disabled` | `boolean \| PropertyDisabledConfig` | Disable with optional tooltip | | `hideFromCollection` | `boolean` | Hide from table view | | `defaultValue` | `any` | Default value for new entities | | `validation` | `object` | Validation rules | | `Field` | `React.ComponentType` | Custom field component | | `Preview` | `React.ComponentType` | Custom table cell component | | `propertyConfig` | `string` | Registered property config key | | `editable` | `boolean` | Enable inline editing in table (default: true) | ### String Properties ```typescript name: { type: "string", name: "Name", validation: { required: true, min: 2, max: 200 } } description: { type: "string", name: "Description", multiline: true, // Textarea markdown: true // Markdown editor } category: { type: "string", name: "Category", enum: [ { id: "electronics", label: "Electronics", color: "blueDark" }, { id: "clothing", label: "Clothing", color: "pinkLight" }, ] } email: { type: "string", name: "Email", email: true, // Email validation validation: { required: true } } website: { type: "string", name: "Website", url: true // URL validation } avatar: { type: "string", name: "Avatar", storage: { // File upload storagePath: "avatars", acceptedFiles: ["image/*"], maxSize: 2 * 1024 * 1024 } } ``` #### String Options | Property | Type | Description | |----------|------|-------------| | `multiline` | `boolean` | Render as textarea | | `markdown` | `boolean` | Render as markdown editor | | `email` | `boolean` | Email format validation | | `url` | `boolean` | URL format validation | | `storage` | `StorageConfig` | Enable file upload | | `enum` | `EnumValues` | Render as select dropdown | | `multiSelect` | `boolean` | Allow multiple enum selections | | `columnType` | `string` | Database column: `"varchar"`, `"text"` | | `isId` | `string` | ID generation: `"uuid"`, `"cuid"`, `"increment"`, `"manual"` | | `userSelect` | `boolean` | Render as a user picker | ### Number Properties ```typescript price: { type: "number", name: "Price", validation: { required: true, min: 0 } } quantity: { type: "number", name: "Quantity", columnType: "integer" // Store as integer } ``` #### Number Options | Property | Type | Description | |----------|------|-------------| | `enum` | `EnumValues` | Render as select with numeric values | | `columnType` | `string` | `"integer"`, `"bigint"`, `"numeric"`, `"serial"`, `"smallint"` | | `isId` | `string` | ID generation strategy | ### Boolean Properties ```typescript active: { type: "boolean", name: "Active", defaultValue: true } ``` ![Switch field](/img/fields/Switch.png) ### Date Properties ```typescript created_at: { type: "date", name: "Created At", autoValue: "on_create", // Set automatically on creation readOnly: true } updated_at: { type: "date", name: "Updated At", autoValue: "on_update" // Set automatically on every save } event_date: { type: "date", name: "Event Date", mode: "date" // Date only (no time) } ``` ![Date field](/img/fields/Date.png) #### Date Options | Property | Type | Description | |----------|------|-------------| | `mode` | `"date" \| "date_time"` | Date only or date + time (default: `"date_time"`) | | `autoValue` | `"on_create" \| "on_update"` | Auto-set timestamps | | `columnType` | `string` | `"timestamp"`, `"date"` | ### Array Properties ```typescript tags: { type: "array", name: "Tags", of: { type: "string" } // Array of strings } images: { type: "array", name: "Images", of: { type: "string", storage: { storagePath: "images", acceptedFiles: ["image/*"] } } } // Block editor (multiple types) content: { type: "array", name: "Content Blocks", oneOf: { properties: { text: { type: "map", properties: { body: { type: "string", name: "Body", markdown: true } } }, image: { type: "map", properties: { src: { type: "string", name: "Image", storage: { ... } }, caption: { type: "string", name: "Caption" } } } } } } ``` ![Block field](/img/fields/Block.png) ### Map Properties ```typescript address: { type: "map", name: "Address", properties: { street: { type: "string", name: "Street" }, city: { type: "string", name: "City" }, zip: { type: "string", name: "ZIP Code" }, country: { type: "string", name: "Country" } } } metadata: { type: "map", name: "Metadata", keyValue: true // Free-form key-value editor } ``` ![Group field](/img/fields/Group.png) ### Enum Values Used with string or number properties to render selects: ```typescript // Simple array enum: ["draft", "published", "archived"] // With labels enum: [ { id: "draft", label: "Draft" }, { id: "published", label: "Published" }, { id: "archived", label: "Archived" } ] // With colors (for Kanban columns and chips) enum: [ { id: "draft", label: "Draft", color: "grayDark" }, { id: "published", label: "Published", color: "greenDark" }, { id: "archived", label: "Archived", color: "orangeDark" } ] ``` ![Select field](/img/fields/Select.png) ### Validation ```typescript validation: { required: true, // Field is required unique: true, // Must be unique in the table requiredMessage: "Custom error message", // String-specific min: 2, // Minimum length max: 200, // Maximum length matches: /^[a-z]+$/, // Regex pattern email: true, // Email format url: true, // URL format // Number-specific min: 0, // Minimum value max: 1000, // Maximum value integer: true, // Must be integer // Array-specific min: 1, // Minimum items max: 10, // Maximum items } ``` ### Conditional Fields Dynamically change property config based on entity values: ```typescript price: { type: "number", name: "Price", dynamicProps: ({ values }) => ({ disabled: values.is_free === true, validation: values.is_free ? {} : { required: true, min: 0 } }) } ``` ### Next Steps - **[Relations](/docs/collections/relations)** — Foreign keys and joins - **[Security Rules](/docs/collections/security-rules)** — Row Level Security - **[Custom Fields](/docs/frontend/custom-fields)** — Build custom field components ## Relations ### Overview Relations define how collections are connected at the database level. They enable Rebase to: - Render **relation picker fields** in entity forms - Resolve **related entities** when displaying previews - Generate **foreign key constraints** in the Drizzle schema - Support **cascade delete/update** behaviors Relations are defined in the `relations` array of a collection: ```typescript const postsCollection: EntityCollection = { slug: "posts", name: "Posts", dbPath: "posts", properties: { title: { type: "string", name: "Title" }, content: { type: "string", name: "Content", multiline: true }, author: { type: "relation", name: "Author", relationName: "author" } }, relations: [ { relationName: "author", target: () => usersCollection, cardinality: "one", localKey: "author_id" } ] }; ``` ### Relation Types #### One-to-One / Many-to-One A foreign key on **this** table points to another table's primary key. ```typescript relations: [ { relationName: "author", target: () => usersCollection, cardinality: "one", // This entity has ONE author direction: "owning", // The FK is on THIS table localKey: "author_id" // Column on the posts table } ] ``` This creates: `posts.author_id → users.id` #### One-to-Many (Inverse) The foreign key is on the **target** table, pointing back to this entity. ```typescript // On the Users collection: relations: [ { relationName: "posts", target: () => postsCollection, cardinality: "many", // This user has MANY posts direction: "inverse", // The FK is on the TARGET table foreignKeyOnTarget: "author_id" // Column on the posts table } ] ``` #### Many-to-Many (Junction Table) Two collections connected through an intermediate junction table. ```typescript // On the Users collection: relations: [ { relationName: "roles", target: () => rolesCollection, cardinality: "many", direction: "owning", through: { table: "user_roles", // Junction table name sourceColumn: "user_id", // FK to this collection targetColumn: "role_id" // FK to target collection } } ] ``` This creates: ```sql CREATE TABLE user_roles ( user_id INTEGER REFERENCES users(id), role_id INTEGER REFERENCES roles(id), PRIMARY KEY (user_id, role_id) ); ``` ### Relation Properties To render a relation field in a form, add a property with `type: "relation"`: ```typescript properties: { author: { type: "relation", name: "Author", relationName: "author", // Must match a relation in the relations array widget: "select" // "select" (dropdown) or "dialog" (full picker) } } ``` ### Multi-Hop Joins For complex relationships that traverse multiple tables, use `joinPath`: ```typescript // Users → Permissions through Roles relations: [ { relationName: "permissions", target: () => permissionsCollection, cardinality: "many", joinPath: [ { table: "user_roles", on: { from: "id", to: "user_id" } }, { table: "roles", on: { from: "role_id", to: "id" } }, { table: "role_permissions", on: { from: "id", to: "role_id" } }, { table: "permissions", on: { from: "permission_id", to: "id" } } ] } ] ``` #### Composite Key Joins ```typescript joinPath: [ { table: "customers", on: { from: ["company_code", "region_id"], // Multiple columns to: ["code", "region_id"] } } ] ``` ### Cascade Rules Control what happens when related entities are updated or deleted: ```typescript relations: [ { relationName: "author", target: () => usersCollection, cardinality: "one", localKey: "author_id", onDelete: "cascade", // Delete posts when user is deleted onUpdate: "cascade" // Update FK when user ID changes } ] ``` | Action | Behavior | |--------|----------| | `"cascade"` | Propagate the change to related rows | | `"restrict"` | Prevent the operation if related rows exist | | `"no action"` | Same as restrict (defer to constraint check) | | `"set null"` | Set the FK column to NULL | | `"set default"` | Set the FK column to its default value | ### Full Relation Interface ```typescript interface Relation { relationName?: string; target: () => EntityCollection; cardinality: "one" | "many"; direction?: "owning" | "inverse"; inverseRelationName?: string; localKey?: string; foreignKeyOnTarget?: string; through?: { table: string; sourceColumn: string; targetColumn: string; }; joinPath?: JoinStep[]; onUpdate?: "cascade" | "restrict" | "no action" | "set null" | "set default"; onDelete?: "cascade" | "restrict" | "no action" | "set null" | "set default"; overrides?: Partial; validation?: { required?: boolean }; } ``` ### Next Steps - **[Security Rules](/docs/collections/security-rules)** — Row Level Security - **[Properties](/docs/collections/properties)** — Property types reference ## Security Rules (RLS) ### Overview Security rules let you define **Row Level Security (RLS)** policies for your PostgreSQL tables directly in your collection definitions. When the Drizzle schema is generated, Rebase creates the corresponding `CREATE POLICY` statements. ```typescript const postsCollection: EntityCollection = { slug: "posts", dbPath: "posts", properties: { /* ... */ }, securityRules: [ { operation: "select", access: "public" }, { operations: ["insert", "update", "delete"], ownerField: "author_id" } ] }; ``` ### How It Works 1. You define `securityRules` on a collection 2. `rebase schema generate` creates Drizzle schema with RLS enabled 3. `rebase db push` or `rebase db migrate` applies the policies to PostgreSQL 4. Every query is filtered by the current user's context automatically The authenticated user's identity is available in SQL via: | Function | Returns | |----------|---------| | `auth.uid()` | The current user's ID | | `auth.roles()` | Comma-separated app role IDs | | `auth.jwt()` | Full JWT claims as JSONB | These are set automatically per-transaction by the Rebase backend. ### Convenience Shortcuts #### Owner-based Access The simplest pattern — users can only access rows they own: ```typescript securityRules: [ { operation: "all", ownerField: "user_id" } ] ``` This generates: `USING (user_id = auth.uid())` #### Public Access Allow anyone (including unauthenticated users) to read: ```typescript securityRules: [ { operation: "select", access: "public" } ] ``` This generates: `USING (true)` #### Authenticated Access Allow any authenticated user: ```typescript securityRules: [ { operation: "select", access: "authenticated" } ] ``` #### Role-based Access Restrict operations to specific roles: ```typescript securityRules: [ { operation: "all", roles: ["admin"] }, { operation: "select", roles: ["editor", "viewer"] } ] ``` ### Raw SQL Expressions For complex logic, use `using` and `withCheck`: ```typescript securityRules: [ { operation: "select", using: "EXISTS (SELECT 1 FROM org_members WHERE org_members.org_id = {org_id} AND org_members.user_id = auth.uid())" } ] ``` - **`using`** — Filters which existing rows are visible (applies to SELECT, UPDATE, DELETE) - **`withCheck`** — Validates new row values (applies to INSERT, UPDATE) Column references use `{column_name}` syntax which gets resolved to the full table-qualified column. ### Combining Shortcuts and SQL Mix convenience shortcuts with raw SQL: ```typescript securityRules: [ // Admins can do anything { operation: "all", roles: ["admin"], using: "true" }, // Regular users can only see their own rows { operation: "select", ownerField: "user_id" }, // Users can insert, but only for themselves { operation: "insert", withCheck: "{user_id} = auth.uid()" }, // Locked rows cannot be updated { operation: "update", mode: "restrictive", using: "{is_locked} = false" } ] ``` ### Permissive vs Restrictive PostgreSQL has two policy modes: - **Permissive** (default) — Multiple permissive policies are **OR'd** together. If any one passes, access is granted. - **Restrictive** — Restrictive policies are **AND'd** together. All must pass. ```typescript securityRules: [ // Permissive: owners can access their rows { operation: "all", ownerField: "user_id" }, // Restrictive: but locked rows cannot be updated { operation: "update", mode: "restrictive", using: "{is_locked} = false", withCheck: "{is_locked} = false" } ] ``` ### Operations | Operation | SQL Equivalent | Description | |-----------|---------------|-------------| | `"select"` | `SELECT` | Read rows | | `"insert"` | `INSERT` | Create new rows | | `"update"` | `UPDATE` | Modify existing rows | | `"delete"` | `DELETE` | Remove rows | | `"all"` | All of the above | Shorthand for all operations | You can also use `operations` (plural) to apply one rule to multiple operations: ```typescript { operations: ["insert", "update", "delete"], ownerField: "author_id" } ``` ### Full SecurityRule Interface ```typescript interface SecurityRule { name?: string; // Human-readable policy name operation?: SecurityOperation; // Single operation operations?: SecurityOperation[]; // Multiple operations mode?: "permissive" | "restrictive"; // Default: "permissive" access?: "public" | "authenticated"; ownerField?: string; // Column containing the owner user ID roles?: string[]; // App roles that this policy applies to using?: string; // Raw SQL USING expression withCheck?: string; // Raw SQL WITH CHECK expression } ``` ### Examples #### Blog Platform ```typescript securityRules: [ // Anyone can read published posts { operation: "select", access: "public", using: "{status} = 'published'" }, // Authors can see their own drafts { operation: "select", ownerField: "author_id" }, // Authors can create and edit their own posts { operations: ["insert", "update"], ownerField: "author_id" }, // Only admins can delete { operation: "delete", roles: ["admin"] } ] ``` #### Multi-Tenant SaaS ```typescript securityRules: [ { operation: "all", using: "EXISTS (SELECT 1 FROM org_members WHERE org_members.org_id = {org_id} AND org_members.user_id = auth.uid())" } ] ``` ### Next Steps - **[Relations](/docs/collections/relations)** — Foreign keys and joins - **[Entity Callbacks](/docs/collections/callbacks)** — Lifecycle hooks ## Entity Callbacks ### Overview Callbacks let you hook into the entity lifecycle to: - **Transform data** before saving (computed fields, slugification) - **Validate** business rules beyond schema validation - **Trigger side effects** after writes (send emails, sync APIs, update caches) - **Filter/transform** data after reading ### Defining Callbacks ```typescript const articlesCollection: EntityCollection = { slug: "articles", callbacks: { beforeSave: async ({ values, entityId, status }) => { // Auto-generate slug from title if (values.title) { values.slug = values.title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } // Set timestamps if (status === "new") { values.created_at = new Date(); } values.updated_at = new Date(); return values; }, afterSave: async ({ values, entityId }) => { // Send notification console.log(`Article ${entityId} saved: ${values.title}`); }, beforeDelete: async ({ entityId }) => { // Prevent deletion of published articles // Throw to block the deletion }, afterRead: async ({ entity }) => { // Transform data after loading return entity; } }, properties: { /* ... */ } }; ``` ### Callback Reference #### `beforeSave` Called before an entity is written to the database. Return the modified values. ```typescript beforeSave: async ({ values, // Entity values entityId, // Entity ID (null for new entities) status, // "new" | "existing" | "copy" previousValues, // Previous values (for updates) context // Full Rebase context }) => { // Return modified values return { ...values, updated_at: new Date() }; } ``` Throw an error to **block the save**: ```typescript beforeSave: async ({ values }) => { if (values.price < 0) { throw new Error("Price cannot be negative"); } return values; } ``` #### `afterSave` Called after a successful save. Use for side effects. ```typescript afterSave: async ({ values, // Saved values entityId, // Entity ID previousValues, // Previous values (null for new entities) status, // "new" | "existing" | "copy" context }) => { // Send webhook await fetch("https://api.slack.com/webhook", { method: "POST", body: JSON.stringify({ text: `New article: ${values.title}` }) }); } ``` #### `afterSaveError` Called when a save operation fails. ```typescript afterSaveError: async ({ values, entityId, error, context }) => { console.error("Save failed:", error); } ``` #### `afterRead` Called after reading entities from the database. Transform the data for display. ```typescript afterRead: async ({ entity, // The entity to transform context }) => { // Add computed fields return { ...entity, values: { ...entity.values, displayName: `${entity.values.first_name} ${entity.values.last_name}` } }; } ``` #### `beforeDelete` Called before an entity is deleted. Throw to block deletion. ```typescript beforeDelete: async ({ entityId, entity, context }) => { if (entity.values.status === "published") { throw new Error("Cannot delete published articles. Unpublish first."); } } ``` #### `afterDelete` Called after a successful deletion. ```typescript afterDelete: async ({ entityId, entity, context }) => { // Cleanup related data console.log(`Article ${entityId} deleted`); } ``` ### Property Callbacks You can also define callbacks at the property level for field-specific transformations: ```typescript properties: { email: { type: "string", name: "Email", callbacks: { beforeSave: ({ value }) => value?.toLowerCase().trim(), afterRead: ({ value }) => value // Could decrypt, etc. } } } ``` ### Next Steps - **[Security Rules](/docs/collections/security-rules)** — Row Level Security - **[Entity History](/docs/backend/history)** — Audit trail ## Backend Overview ### Overview The Rebase backend is a **Node.js server** built on [Hono](https://hono.dev/) that provides: - **REST API** — Auto-generated CRUD endpoints for each collection - **Authentication** — JWT tokens, Google OAuth, user/role management - **Storage** — File upload/download with local filesystem or S3 - **WebSocket** — Real-time data sync via PostgreSQL LISTEN/NOTIFY - **Entity History** — Audit trail for every data change Everything is initialized with a single function: ```typescript const instance = await initializeRebaseBackend({ app, server, collections, driver: { connection: db, schema: { tables, enums, relations } }, auth: { jwtSecret: process.env.JWT_SECRET!, }, storage: { type: "local", basePath: "./uploads" }, history: true, enableSwagger: process.env.NODE_ENV !== "production" }); ``` ### What Gets Created After initialization, these routes are mounted: | Path | Purpose | |------|---------| | `/api/auth/*` | Authentication (signup, login, refresh, Google OAuth) | | `/api/admin/*` | User and role management (admin-only) | | `/api/storage/*` | File upload, download, and deletion | | `/api/data/collections` | Collection metadata endpoint | | `/api/data/:slug` | CRUD operations per collection (GET, POST, PUT, DELETE) | | `/api/data/:slug/:id/history` | Entity change history (when enabled) | | `/api/data/docs` | OpenAPI spec (when `enableSwagger: true`) | | `/api/data/swagger` | Swagger UI (dev mode, when `enableSwagger: true`) | | WebSocket on upgrade | Real-time subscriptions | ### Configuration Reference ```typescript interface RebaseBackendConfig { // HTTP framework app: Hono; // Hono application instance server: Server; // Node.js HTTP server (for WebSocket attachment) basePath?: string; // Route prefix (default: "/api") // Collections collections?: EntityCollection[]; // Your collection definitions collectionsDir?: string; // Auto-load collections from a directory // Database driver (see Driver Configuration) driver: DriverConfig | Record; // Authentication auth?: AuthConfig; // File storage storage?: BackendStorageConfig | Record; // Entity history history?: boolean | HistoryConfig; // OpenAPI/Swagger enableSwagger?: boolean; // Logging logging?: { level?: "error" | "warn" | "info" | "debug" }; } ``` ### The Backend Instance `initializeRebaseBackend` returns a `RebaseBackendInstance` with access to all services: ```typescript const instance = await initializeRebaseBackend(config); // Access services instance.driver // Default data driver instance.driverRegistry // All drivers (for multi-database) instance.realtimeService // Default realtime service instance.userService // User management instance.roleService // Role management instance.storageController // Default storage instance.storageRegistry // All storage backends instance.collectionRegistry // Collection metadata instance.historyService // Entity history ``` ### REST API The REST API is auto-generated from your collections. Every collection gets these endpoints: | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/data/:slug` | List entities (with filter, sort, limit, search) | | `GET` | `/api/data/:slug/:id` | Get a single entity | | `POST` | `/api/data/:slug` | Create a new entity | | `PUT` | `/api/data/:slug/:id` | Update an entity | | `DELETE` | `/api/data/:slug/:id` | Delete an entity | #### Query Parameters | Param | Description | Example | |-------|-------------|---------| | `filter` | JSON-encoded filter conditions | `?filter={"active":["==",true]}` | | `orderBy` | Sort field | `?orderBy=created_at` | | `order` | Sort direction | `?order=desc` | | `limit` | Page size | `?limit=25` | | `startAfter` | Cursor for pagination | `?startAfter=encodedCursor` | | `search` | Full-text search | `?search=laptop` | ### WebSocket The WebSocket server attaches to the same HTTP server and provides real-time subscriptions: - Subscribe to **collection changes** — get notified when any entity in a collection is created, updated, or deleted - Subscribe to **entity changes** — get notified when a specific entity changes - Automatic **reconnection** handling in the client SDK The backend uses PostgreSQL `LISTEN/NOTIFY` internally. For multi-instance deployments, provide a `connectionString` in your driver config to enable cross-instance broadcasting. ### Error Handling The backend includes an error handler that catches all exceptions and returns structured error responses: ```json { "error": { "message": "Entity not found", "code": "not-found", "status": 404 } } ``` If initialization fails (e.g., database connection error), the server still starts but returns 503 for all API requests, with a descriptive error message in the logs. ### Next Steps - **[Authentication](/docs/auth)** — JWT, Google OAuth, user management - **[Storage](/docs/storage)** — Local and S3 file storage - **[Entity Callbacks](/docs/collections/callbacks)** — Lifecycle hooks - **[Entity History](/docs/backend/history)** — Audit trail ## Entity History ### Overview Entity history records a snapshot of entity values on every create, update, and delete. This gives you a full audit trail with diffs. ### Enabling History #### Backend Enable history in `initializeRebaseBackend`: ```typescript await initializeRebaseBackend({ // ... history: true }); ``` Or with custom retention settings: ```typescript history: { maxEntries: 200, // Per entity, oldest pruned first (default: 200) ttlDays: 90 // Entries older than this are pruned (default: 90) } ``` #### Per Collection Mark which collections should track history: ```typescript const ordersCollection: EntityCollection = { slug: "orders", history: true, // Enable for this collection properties: { /* ... */ } }; ``` ### How It Works 1. The backend creates a `rebase.entity_history` table automatically 2. On every create, update, or delete, a snapshot is recorded with: - Entity ID, collection slug, and table name - The full entity values (before and after) - Timestamp and user ID - Operation type (`insert`, `update`, `delete`) 3. Old entries are pruned periodically (every 6 hours) ### REST Endpoint ``` GET /api/data/:slug/:entityId/history ``` Returns a list of history entries for a specific entity, ordered by most recent first: ```json { "data": [ { "id": 42, "entity_id": "123", "collection_slug": "orders", "operation": "update", "values": { "status": "shipped", "total": 99.99 }, "previous_values": { "status": "pending", "total": 99.99 }, "user_id": "admin-user-id", "created_at": "2025-01-15T10:30:00Z" } ] } ``` ### Retention Configuration | Setting | Default | Description | |---------|---------|-------------| | `maxEntries` | 200 | Maximum entries per entity. Oldest are pruned. | | `ttlDays` | 90 | Entries older than this are deleted. | The backend runs a global prune sweep every 6 hours. ### Next Steps - **[Entity Callbacks](/docs/collections/callbacks)** — Lifecycle hooks - **[Backend Overview](/docs/backend)** — Full backend configuration ## Authentication ### Overview Rebase includes a complete authentication system: - **JWT tokens** — Access and refresh token flow - **Google OAuth** — Sign in with Google - **User management** — Signup, login, password reset - **Role-based access** — Assign roles to users, check permissions in collections - **Auto-bootstrapping** — First user automatically gets admin role ### Backend Configuration ```typescript await initializeRebaseBackend({ // ... auth: { jwtSecret: process.env.JWT_SECRET!, // Required accessExpiresIn: "1h", // Access token lifetime refreshExpiresIn: "30d", // Refresh token lifetime requireAuth: true, // Require auth for data API allowRegistration: false, // Allow new signups google: { clientId: process.env.GOOGLE_CLIENT_ID // Optional }, email: { // Optional — for password reset smtpHost: "smtp.gmail.com", smtpPort: 587, smtpUser: "noreply@example.com", smtpPass: "app-password", from: "Rebase " } } }); ``` Auth tables (`rebase.users`, `rebase.roles`, `rebase.user_roles`, `rebase.refresh_tokens`) are **auto-created** on first startup. ### Auth Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/auth/register` | Create a new account | | `POST` | `/api/auth/login` | Login with email/password | | `POST` | `/api/auth/refresh` | Refresh the access token | | `POST` | `/api/auth/google` | Login with Google OAuth token | | `POST` | `/api/auth/logout` | Revoke refresh token | | `POST` | `/api/auth/forgot-password` | Send password reset email | | `POST` | `/api/auth/reset-password` | Reset password with token | ### Frontend Setup #### Auth Controller ```typescript const client = createRebaseClient({ baseUrl: API_URL, websocketUrl: WS_URL }); const authController = useRebaseAuthController({ client, googleClientId: GOOGLE_CLIENT_ID // Optional }); // Available properties: authController.user // Current user object (or null) authController.initialLoading // True while checking stored session authController.signOut() // Log out authController.getAuthToken() // Get current JWT for API calls ``` #### Login View ```tsx if (!authController.user) { return ( ); } ``` ### User & Role Management #### Backend Services After initialization, the backend instance provides `userService` and `roleService`: ```typescript const { userService, roleService } = instance; // List all users const users = await userService.listUsers(); // Assign a role await roleService.assignRole(userId, roleId); ``` #### Frontend Components Rebase provides built-in views for managing users and roles: ```tsx const userManagement = useBackendUserManagement({ client: rebaseClient, currentUser: authController.user }); // In your routes: } /> } /> ``` ![User management interface](/img/user_management.png) ### Role Simulation (Dev Mode) In developer mode, you can simulate different roles without logging out: ```typescript const effectiveRoleController = useBuildEffectiveRoleController(); // When active, the UI behaves as if the current user has this role effectiveRoleController.setEffectiveRole("editor"); ``` ### First User Bootstrap When no users exist in the database, the first person to register automatically becomes an admin. After that, registration is controlled by the `allowRegistration` setting. This ensures you can always bootstrap a fresh deployment without needing to seed the database manually. ### Next Steps - **[Storage](/docs/storage)** — File storage configuration - **[Collections](/docs/collections)** — Permissions per collection ## Storage ### Overview Rebase provides integrated file storage with two backend options: - **Local filesystem** — Files stored on disk (great for development) - **S3-compatible** — AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces ### Backend Configuration #### Local Storage ```typescript await initializeRebaseBackend({ // ... storage: { type: "local", basePath: "./uploads" // Directory for file storage } }); ``` #### S3 Storage ```typescript await initializeRebaseBackend({ // ... storage: { type: "s3", bucket: "my-media-bucket", region: "us-east-1", accessKeyId: process.env.S3_ACCESS_KEY_ID, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, // Optional: custom endpoint for MinIO, R2, etc. endpoint: "https://s3.example.com" } }); ``` #### Multiple Storage Backends You can configure multiple storage backends and route different fields to different backends: ```typescript storage: { "(default)": { type: "local", basePath: "./uploads" }, "media": { type: "s3", bucket: "media-bucket", region: "us-east-1", ... } } ``` ### Storage Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/storage/upload` | Upload a file | | `GET` | `/api/storage/files/:path` | Download/serve a file | | `DELETE` | `/api/storage/files/:path` | Delete a file | ### Frontend: File Upload Fields To add file uploads to your collections, use the `storage` property on string fields: ```typescript properties: { image: { type: "string", name: "Product Image", storage: { storagePath: "products", // Subdirectory in storage acceptedFiles: ["image/*"], // MIME type filter maxSize: 5 * 1024 * 1024, // 5MB max fileName: (context) => { // Custom filename return context.entityId + "_" + context.file.name; } } }, documents: { type: "array", name: "Documents", of: { type: "string", storage: { storagePath: "documents", acceptedFiles: ["application/pdf", "image/*"] } } } } ``` ![File upload field](/img/fields/File_upload.png) #### Storage Config Options | Property | Type | Description | |----------|------|-------------| | `storagePath` | `string` | Subdirectory within the storage backend | | `acceptedFiles` | `string[]` | Allowed MIME types (e.g., `["image/*"]`, `["application/pdf"]`) | | `maxSize` | `number` | Maximum file size in bytes | | `fileName` | `function` | Custom filename generator | | `metadata` | `object` | Additional metadata to store with the file | | `storeUrl` | `boolean` | Store the full URL instead of the relative path | #### Multiple File Uploads Wrap the storage property in an array for multiple file uploads: ```typescript photos: { type: "array", name: "Photos", of: { type: "string", storage: { storagePath: "photos", acceptedFiles: ["image/*"] } } } ``` ![Multi file upload](/img/fields/Multi_file_upload.png) ### Frontend: useStorageSource Hook For programmatic file operations: ```typescript const storageSource = useStorageSource(); // Upload a file const result = await storageSource.uploadFile({ file, fileName: "my-file.pdf", path: "documents" }); // Get download URL const url = await storageSource.getDownloadURL(result.path); ``` ### Production Tips :::caution **Local storage is not suitable for production deployments** on ephemeral platforms (Cloud Run, Heroku, etc.) where the filesystem is wiped on each deploy. Use S3 for production. ::: - Mount a **persistent volume** if using local storage on Docker/Kubernetes - Use **S3** or compatible (R2, MinIO) for production deployments - Configure a **CDN** (CloudFront, Cloudflare) in front of your S3 bucket for performance ### Next Steps - **[Client SDK](/docs/sdk)** — Programmatic data and file operations - **[Properties](/docs/collections/properties)** — All property types ## Client SDK ### Overview The `@rebasepro/client` package provides a type-safe JavaScript SDK for interacting with your Rebase backend. It handles: - **Data operations** — CRUD with filtering, sorting, and pagination - **Real-time subscriptions** — WebSocket-based live updates - **Authentication** — Token management, login, signup - **Storage** — File upload and download ### Installation ```bash npm install @rebasepro/client ``` ### Setup ```typescript const client = createRebaseClient({ baseUrl: "http://localhost:3001", websocketUrl: "ws://localhost:3001" }); ``` The client automatically manages authentication tokens — once a user logs in, all subsequent requests include the JWT. ### Data Operations #### Fetch a Collection ```typescript const products = await client.data.fetchCollection("products", { filter: { active: ["==", true] }, orderBy: "created_at", order: "desc", limit: 25 }); // products is an array of Entity objects: // { id: 1, values: { name: "Laptop", price: 999 }, path: "products" } ``` #### Fetch a Single Entity ```typescript const product = await client.data.fetchEntity("products", 42); ``` #### Create an Entity ```typescript const newProduct = await client.data.saveEntity("products", { name: "New Product", price: 29.99, active: true }); ``` #### Update an Entity ```typescript await client.data.saveEntity("products", { name: "Updated Name", price: 39.99 }, 42); // entity ID ``` #### Delete an Entity ```typescript await client.data.deleteEntity("products", 42); ``` #### Search ```typescript const results = await client.data.fetchCollection("products", { search: "laptop", limit: 10 }); ``` ### Real-time Subscriptions Subscribe to collection changes via WebSocket: ```typescript // Subscribe to all products const unsubscribe = client.data.listenCollection( "products", { filter: { active: ["==", true] }, limit: 50 }, (entities) => { console.log("Products updated:", entities); } ); // Unsubscribe when done unsubscribe(); ``` Subscribe to a single entity: ```typescript const unsubscribe = client.data.listenEntity( "products", 42, (entity) => { console.log("Product changed:", entity); } ); ``` The WebSocket client handles reconnection automatically. ### Authentication ```typescript // Login const session = await client.auth.signIn("user@example.com", "password"); // Register const session = await client.auth.signUp("user@example.com", "password"); // Google OAuth const session = await client.auth.signInWithGoogle(googleIdToken); // Refresh token await client.auth.refreshToken(); // Logout await client.auth.signOut(); // Get current user const user = client.auth.getUser(); ``` ### Storage ```typescript // Upload const result = await client.storage.uploadFile(file, "products/image.jpg"); // Get URL const url = await client.storage.getDownloadURL("products/image.jpg"); // Delete await client.storage.deleteFile("products/image.jpg"); ``` ### Using with React In a Rebase frontend, the client is typically created once and shared via context: ```tsx const client = createRebaseClient({ baseUrl: API_URL, websocketUrl: WS_URL }); // Pass to Rebase provider ``` Access it from any component: ```tsx function MyComponent() { const client = useRebaseClient(); // Use client.data, client.auth, client.storage } ``` ### SDK Generator Generate a fully typed client SDK from your collection definitions: ```bash rebase generate_sdk ``` This creates TypeScript types for all your entities, so you get autocomplete and type checking when using the client. ### Next Steps - **[Frontend Overview](/docs/frontend)** — React framework and components - **[Backend Overview](/docs/backend)** — Server configuration ## Frontend Overview ### Overview The Rebase frontend is a **React framework** that renders your admin panel. It reads your collection definitions and generates tables, forms, navigation, and routing automatically. The key components that make up a Rebase frontend: ```tsx {({ loading }) => ( )} ``` ### The Rebase Provider `` is the root provider that makes all Rebase functionality available to child components via context. It accepts: | Prop | Description | |------|-------------| | `client` | `RebaseClient` instance for data, auth, and storage | | `collectionRegistryController` | Resolves collection paths and configurations | | `cmsUrlController` | Builds URLs and handles routing | | `navigationStateController` | Manages navigation state, views, and plugins | | `authController` | Authentication state and methods | | `storageSource` | File storage operations | | `userConfigPersistence` | Local UI preferences (column widths, etc.) | | `entityViews` | Global custom entity view tabs | | `entityActions` | Global entity actions | | `plugins` | Plugin instances (legacy prop — prefer passing via navigation controller) | ### Controllers Controllers are React hooks that configure specific aspects of the framework: #### `useBuildNavigationStateController` The main controller that wires everything together: ```typescript const navigationStateController = useBuildNavigationStateController({ collections: () => [...collections], // Collection definitions views: customViews, // Custom navigation views plugins, // Plugin instances authController, data: rebaseClient.data, collectionRegistryController, cmsUrlController, adminMode: adminModeController.mode, userManagement }); ``` #### `useBuildCollectionRegistryController` Manages how collections are resolved from URL paths: ```typescript const collectionRegistryController = useBuildCollectionRegistryController({ userConfigPersistence }); ``` #### `useBuildCMSUrlController` Configures URL generation: ```typescript const cmsUrlController = useBuildCMSUrlController({ basePath: "/", baseCollectionPath: "/c", collectionRegistryController }); ``` #### `useBuildModeController` Manages light/dark theme: ```typescript const modeController = useBuildModeController(); // Provides: modeController.mode ("light" | "dark"), modeController.toggleMode() ``` #### `useBuildAdminModeController` Toggles between Studio and Content modes: ```typescript const adminModeController = useBuildAdminModeController(); // Provides: adminModeController.mode ("studio" | "content") ``` ### Scaffold Components | Component | Description | |-----------|-------------| | `` | Main layout container with responsive sidebar | | `` | Top navigation bar with search, mode toggle, user menu | | `` | Side navigation with collection list and view links | | `` | Container for side panel entity editors | | `` | Route container that integrates with React Router | | `` | Handles collection routes (`/c/*`) | | `` | Default home page showing collection cards | | `` | Studio mode home page with developer tools | ### Custom Views Add top-level navigation views for dashboards, tools, or custom pages: ```typescript const views: CMSView[] = [ { slug: "dashboard", name: "Dashboard", icon: "dashboard", group: "Analytics", view: }, { slug: "settings", name: "App Settings", icon: "settings", view: , nestedRoutes: true // Support sub-paths } ]; ``` ### Styling Rebase uses **Tailwind CSS v4** and supports light/dark modes. Customize via: - **CSS custom properties** — Override design tokens - **`ModeControllerProvider`** — Control light/dark mode - **Tailwind config** — Standard Tailwind customization ```css /* Override design tokens */ :root { --font-sans: "Inter", sans-serif; --font-mono: "JetBrains Mono", monospace; } ``` ### Next Steps - **[Custom Fields](/docs/frontend/custom-fields)** — Build custom form fields - **[Entity Views](/docs/frontend/entity-views)** — Add tabs to entity editors - **[View Modes](/docs/frontend/view-modes)** — Table, Cards, Kanban - **[Plugins](/docs/plugins)** — Extend the framework ## View Modes ### Overview Every collection can be displayed in three view modes: - **Table** — Spreadsheet-style grid with inline editing, sorting, filtering - **Cards** — Card grid for visual content (images, previews) - **Kanban** — Drag-and-drop board grouped by an enum property ### Configuration ```typescript const productsCollection: EntityCollection = { slug: "products", defaultViewMode: "table", // Default view enabledViews: ["table", "kanban"], // Available views kanban: { columnProperty: "status", // Enum property for columns orderProperty: "sort_order" // Property for drag-and-drop ordering }, // ... }; ``` ### Table View The default view is a high-performance virtualized spreadsheet with: - **Inline editing** — Click any cell to edit in-place - **Column resizing** — Drag column headers - **Column reordering** — Drag to rearrange - **Sorting** — Click column headers - **Text search** — Full-text search across string fields - **Filtering** — Per-column filters - **Multi-select** — Select entities for bulk actions #### Row Height Control row height with `defaultSize`: | Size | Pixels | Best for | |------|--------|----------| | `"xs"` | 40 | Dense data tables | | `"s"` | 54 | Default | | `"m"` | 80 | With image thumbnails | | `"l"` | 120 | Cards with previews | | `"xl"` | 260 | Rich content previews | ### Kanban View Configure a Kanban board by specifying which enum property to use as columns: ```typescript const tasksCollection: EntityCollection = { slug: "tasks", defaultViewMode: "kanban", kanban: { columnProperty: "status", orderProperty: "sort_order" }, properties: { title: { type: "string", name: "Title" }, status: { type: "string", name: "Status", enum: [ { id: "backlog", label: "Backlog", color: "grayDark" }, { id: "in_progress", label: "In Progress", color: "blueDark" }, { id: "review", label: "Review", color: "orangeDark" }, { id: "done", label: "Done", color: "greenDark" } ] }, sort_order: { type: "number", name: "Sort Order" } } }; ``` Drag-and-drop between columns automatically updates the enum field and sort order. ### Cards View Cards display entities as visual cards — useful for image-heavy content: ```typescript const articlesCollection: EntityCollection = { slug: "articles", defaultViewMode: "cards", properties: { title: { type: "string", name: "Title" }, cover: { type: "string", name: "Cover Image", storage: { storagePath: "covers", acceptedFiles: ["image/*"] } } } }; ``` ### Next Steps - **[Entity Views](/docs/frontend/entity-views)** — Custom tabs on entity forms - **[Entity Actions](/docs/frontend/entity-actions)** — Custom entity actions ## Custom Fields ### Overview Rebase generates form fields automatically based on property types. For custom behavior, you can build your own fields. ### Creating a Custom Field A custom field is a React component that receives `FieldProps`: ```tsx function ColorPickerField({ value, setValue, error, showError }: FieldProps) { return (
setValue(e.target.value)} /> {showError && error && {error}}
); } ``` #### FieldProps | Prop | Type | Description | |------|------|-------------| | `value` | `T` | Current field value | | `setValue` | `(value: T) => void` | Update the field value | | `error` | `string` | Validation error message | | `showError` | `boolean` | Whether to display the error | | `isSubmitting` | `boolean` | Form is being saved | | `property` | `Property` | The property configuration | | `context` | `FormContext` | Full form context with all entity values | | `disabled` | `boolean` | Field is readonly | | `tableMode` | `boolean` | Rendering inside the spreadsheet (compact mode) | ### Registering a Custom Field #### Per-Property Register on a single property: ```typescript properties: { brand_color: { type: "string", name: "Brand Color", Field: ColorPickerField } } ``` #### Global Property Config Register a reusable field type: ```typescript const colorPropertyConfig: PropertyConfig = { key: "color_picker", name: "Color Picker", Field: ColorPickerField, property: { type: "string" } }; // Register globally ``` Then use it in any collection: ```typescript properties: { color: { type: "string", name: "Color", propertyConfig: "color_picker" } } ``` ### Accessing Form Context Custom fields can access the full entity values: ```tsx function PriceWithTaxField({ value, setValue, context }: FieldProps) { const taxRate = context.values.tax_rate ?? 0.1; const priceWithTax = value ? value * (1 + taxRate) : 0; return (
setValue(Number(e.target.value))} />

With tax: ${priceWithTax.toFixed(2)}

); } ``` ### Table Mode When rendering inside the spreadsheet view, fields should be compact. Check `tableMode`: ```tsx function MyField({ value, setValue, tableMode }: FieldProps) { if (tableMode) { return { /* open editor */ }}>{value}; } return (