| name | rebase-collections |
| description | Comprehensive guide for defining Rebase collections, property types, validation, and schema configuration. Use this skill when the user needs help creating collections, adding properties, configuring field types, or understanding the schema-as-code approach. |
Rebase Collections
Rebase collections are the core building blocks of your data model. They define the structure, validation, and UI configuration of your data — all in TypeScript.
Core Concepts
Collections
A collection is defined as a TypeScript object implementing the PostgresCollectionConfig interface from @rebasepro/types. Each collection maps to a database table (via the table property) and generates:
- Full CRUD REST endpoints at
/api/data/{slug}
- Optional GraphQL queries and mutations
- Admin panel views (table, forms, cards, kanban, list)
Properties
Properties define the fields of your collection. Rebase supports these built-in property types:
| Type | Description | PostgreSQL Column |
|---|
string | Text fields, URLs, emails, markdown, file uploads | VARCHAR / TEXT |
number | Integers and decimals | INTEGER / DOUBLE PRECISION |
boolean | True/false toggles | BOOLEAN |
date | Date and datetime values | TIMESTAMP |
map | Nested objects (JSON) | JSONB |
array | Lists of values | JSONB or native arrays |
relation | Foreign key to another collection (SQL JOINs) | FK column or junction table |
reference | Legacy FK reference by collection slug (Firestore-style) | UUID with FK |
geopoint | Latitude/longitude pairs | JSONB |
vector | Embedding vectors for similarity search | VECTOR |
Reference vs Relation
IMPORTANT FOR AGENTS: Understand the difference between reference and relation — they are NOT interchangeable.
| Feature | relation (Recommended) | reference (Legacy) |
|---|
| Backend | SQL JOINs, FK constraints | Stores a collection path + entity ID |
| Cascade rules | onDelete, onUpdate | None |
| Junction tables | Yes (many-to-many) | No |
| Multi-hop joins | Yes (joinPath) | No |
| Inverse lookups | Yes (kind: "hasMany" / "hasOne") | No |
| Where to use | PostgresCollectionConfig | FirebaseCollectionConfig or legacy |
| Stored value | FK column(s) managed by framework | { id, path } object or string |
Use relation for all new Postgres collections. The reference type exists for backward compatibility with Firestore-style collections.
A string property can also act as a lightweight reference via its reference sub-property (stores just the ID string and renders a reference picker), but this does not create SQL JOINs.
Schema-as-Code
Collections are defined as standalone TypeScript files under config/collections/ relative to the project root. The visual Studio edits these files via AST manipulation — it never runs raw SQL. This preserves custom callbacks and complex configuration.
Defining a Collection
import { PostgresCollectionConfig } from "@rebasepro/types";
const productsCollection: PostgresCollectionConfig = {
name: "Products",
singularName: "Product",
slug: "products",
table: "products",
description: "Product catalog with pricing and inventory",
history: true,
properties: {
id: {
name: "ID",
type: "number",
isId: "increment"
},
name: {
name: "Product Name",
type: "string",
validation: { required: true }
},
price: {
name: "Price",
type: "number",
validation: { required: true, min: 0 }
},
description: {
name: "Description",
type: "string",
admin: { multiline: true }
},
published: {
name: "Published",
type: "boolean",
defaultValue: false
},
created_at: {
name: "Created At",
type: "date",
mode: "date_time",
autoValue: "on_create",
admin: { readOnly: true, hideFromCollection: true }
},
category: {
name: "Category",
type: "string",
enum: [
{ id: "electronics", label: "Electronics", color: "blue" },
{ id: "clothing", label: "Clothing", color: "purple" },
{ id: "books", label: "Books", color: "green" }
]
}
},
admin: {
icon: "ShoppingBag",
group: "E-Commerce",
defaultViewMode: "table",
enabledViews: ["table", "cards"],
openEntityMode: "split",
inlineEditing: true,
exportable: true,
selectionEnabled: true,
propertiesOrder: [
"name", "price", "category", "description",
"published", "created_at"
]
}
};
export default productsCollection;
Presentation lives under admin. Keys written admin.x below go inside a nested
admin: { … } block, not at the top level. The backend never reads inside it, which is
what lets a BaaS or headless project have no React in its dependency tree at all.
Annotate the collection with AdminCollectionConfig (a type-only import from
@rebasepro/admin-types) to get the block type-checked — with plain CollectionConfig
it is opaque and a typo compiles.
Collection Options
| Option | Type | Default | Description |
|---|
name | string | — | Display name (plural). E.g. "Products" |
singularName | string | — | Singular display name. E.g. "Product" |
slug | string | — | URL slug for API and routing |
table | string | — | PostgreSQL table name |
schema | string | "public" | PostgreSQL schema name |
description | string | — | Description shown in the UI (supports Markdown) |
admin.icon | string | ReactNode | — | Lucide icon name or React element |
admin.group | string | "Views" | Sidebar group heading |
dataSource | string | "(default)" | Data-source key — routes the collection to a backend registered on <Rebase dataSources> / initializeRebaseBackend({ dataSources }). See Data sources & multiple backends below. |
driver | string | undefined | Deprecated — engine hint ("postgres"/"firestore"/"mongodb"). Prefer dataSource. When dataSource is omitted, driver doubles as the routing key. |
databaseId | string | — | Physical DB/schema/Firestore-database within the engine |
history | boolean | false | Enable entity audit trail (requires history plugin) |
admin.defaultViewMode | ViewMode | "table" | Default view: "table", "cards", "kanban", "list" |
admin.enabledViews | ViewMode[] | ["table","cards","kanban"] | Enabled view modes |
admin.openEntityMode | "split" | "side_panel" | "full_screen" | "dialog" | "full_screen" | How entities open when clicked |
admin.defaultEntityAction | "edit" | "view" | "edit" | Click behavior: open form or read-only view |
admin.kanban | { columnProperty: string } | — | Kanban column config (requires enum property) |
admin.propertiesOrder | string[] | — | Field display order in forms and table |
admin.entityViews | (string | EntityCustomView)[] | — | Custom tabs on entity detail |
admin.titleProperty | string | first text prop | Property used as entity title in previews |
admin.previewProperties | string[] | — | Properties shown when this collection is referenced |
admin.listProperties | string[] | — | Columns to display in list view |
admin.selectionEnabled | boolean | — | Enable row selection checkboxes |
admin.selectionController | SelectionController | — | External selection state controller |
admin.inlineEditing | boolean | — | Allow inline editing in collection table view |
admin.exportable | boolean | ExportConfig | — | Enable data export. true for default, or ExportConfig for custom fields |
admin.pagination | boolean | number | true (50) | Enable pagination. Set a number to customize page size |
admin.defaultSize | "xs" | "s" | "m" | "l" | "xl" | — | Default rendered row size |
admin.fixedFilter | FilterValues | — | Permanent filter that cannot be changed by users |
admin.defaultFilter | FilterValues | — | Initial filter (can be changed by users) |
admin.filterPresets | FilterPreset[] | — | Quick-access filter buttons in toolbar |
admin.sort | [string, "asc" | "desc"] | — | Default sort order. E.g. ["created_at", "desc"] |
admin.orderProperty | string | — | Property key for drag-and-drop ordering (Kanban/general) |
admin.formAutoSave | boolean | false | Auto-save form on field change |
admin.formView | FormViewConfig | — | Custom component replacing the default entity form |
admin.hideFromNavigation | boolean | false | Hide from sidebar (still accessible via URL) |
admin.hideIdFromForm | boolean | false | Hide ID field in entity form |
admin.hideIdFromCollection | boolean | false | Hide ID column in collection table |
admin.defaultSelectedView | string | Function | — | Auto-open a custom view/subcollection tab |
admin.sideDialogWidth | number | string | — | Width of side dialog in pixels |
admin.alwaysApplyDefaultValues | boolean | false | Re-apply defaults on every update |
admin.includeJsonView | boolean | false | Show a JSON tab in entity detail |
admin.localChangesBackup | "manual_apply" | "auto_apply" | false | "manual_apply" | Local changes backup strategy |
admin.disableDefaultActions | ("edit" | "copy" | "delete")[] | — | Disable built-in actions |
admin.additionalFields | AdditionalFieldDelegate[] | — | Virtual computed columns for views |
admin.entityActions | EntityAction[] | — | Custom action buttons (see Entity Actions section) |
admin.Actions | ComponentRef[] | — | Custom toolbar action components |
callbacks | CollectionCallbacks<M, USER> | — | Lifecycle hooks (see Collection Callbacks section) |
relations | Relation[] | — | Explicit relation definitions (usually auto-extracted from properties) |
securityRules | SecurityRule[] | — | Row Level Security policies |
childCollections | () => CollectionConfig[] | — | Nested child collections (populated automatically) |
overrides | EntityOverrides | — | Override data source or storage source |
ownerId | string | — | Owner user ID (for plugins/custom code) |
auth | `boolean | AuthCollectionConfig` | — |
admin.components | CollectionComponentOverrideMap | — | Collection-scoped UI component overrides |
Data sources & multiple backends
A collection lives in a data source identified by collection.dataSource
(default "(default)"). A data source has an engine (postgres,
mongodb, firestore, custom → drives editor capabilities) and a transport:
server — through the Rebase backend/client. Covers Postgres, MongoDB,
and any server-mediated engine. This is the default; such collections need no
registration.
direct — straight from the client to an external backend via its SDK
(e.g. Firestore). The Rebase backend is not in the data path.
custom — a developer-supplied DataDriver.
Register the non-default (direct/custom) sources once; server engines ride the
client and only need a backend bootstrapper.
<Rebase
client={rebaseClient}
dataSources={[
{ key: "analytics", engine: "firestore", transport: "direct", driver: firestoreDriver }
]}
/>
{ slug: "events", dataSource: "analytics", properties: { } }
initializeRebaseBackend({
bootstrappers: [pgBootstrapper , mongoBootstrapper],
dataSources: [{ key: "analytics", engine: "firestore", transport: "direct" }],
collections: [
{ slug: "products" },
{ slug: "orders", driver: "mongodb" }
],
});
Routing is automatic and resolved by collection path: list/entity views,
references, board, import/export, and context.data all hit the right backend
with no per-collection wiring. The data-source key matches the backend
bootstrapper id/type (e.g. "mongodb"). RLS is applied per-engine where
supported. The deprecated drivers={{ key: driver }} prop is a shorthand for a
single direct dataSources entry.
Migration note: collection-level driver is deprecated in favor of
dataSource. It still works (and provides the engine hint), so existing
Firestore collections using driver: "firestore" keep functioning.
Common Property Options (BaseProperty)
All property types share these base options:
| Option | Type | Default | Description |
|---|
name | string | — | Display label for the field |
description | string | — | Help text displayed under the field |
columnName | string | auto from key | Explicit DB column name (bypasses snake_case conversion) |
validation | PropertyValidationSchema | — | Validation rules (see below) |
defaultValue | unknown | — | Default value for new entities |
propertyConfig | string | — | Reuse a globally defined property config by key |
dynamicProps | (props) => Partial<Property> | — | Dynamic property overrides based on entity values |
conditions | PropertyConditions | — | JSON Logic-based declarative conditions |
callbacks | PropertyCallbacks | — | Per-field afterRead and beforeSave hooks |
UI Options (AdminPropertyOptions)
All property types support a admin object for display configuration:
| Option | Type | Default | Description |
|---|
ui.columnWidth | number | — | Column width in pixels (table view) |
admin.hideFromCollection | boolean | — | Hide from collection table view |
ui.readOnly | boolean | — | Render as read-only preview |
ui.disabled | boolean | PropertyDisabledConfig | — | Disable editing |
ui.widthPercentage | number | — | Width as percentage of form |
ui.customProps | unknown | — | Custom props passed to the field component |
admin.Field | ComponentRef | — | Custom field component |
admin.Preview | ComponentRef | — | Custom preview/cell component |
String Properties
title: {
name: "Title",
type: "string",
validation: { required: true, min: 3, max: 200 },
multiline: false
}
String-Specific Options
| Option | Type | Default | Description |
|---|
columnType | "varchar" | "text" | "char" | "uuid" | "varchar" | Database column type |
isId | boolean | "manual" | "uuid" | "cuid" | string | — | Mark as primary key with generation strategy |
enum | EnumValues | — | Dropdown/picklist values |
multiline | boolean | false | Multi-line text area |
markdown | boolean | false | Markdown editor with preview. Uses the RichTextEditor component (import { RichTextEditor } from "@rebasepro/admin/editor") — a full WYSIWYG editor supporting Markdown, JSON, and HTML output. |
url | boolean | PreviewType | — | Render as link. PreviewType: "image", "video", "audio", "file" |
email | boolean | — | Email field rendering |
storage | StorageConfig | — | File upload configuration (see Storage section) |
userSelect | boolean | — | Render as user picker (value = user ID) |
previewAsTag | boolean | — | Render value as a colored tag/chip |
reference | ReferenceProperty | — | Lightweight reference to another collection by ID |
String isId Strategies
| Value | Behavior |
|---|
true / "manual" | User-defined ID, must be entered manually |
"uuid" | Auto-generated UUID via gen_random_uuid() |
"cuid" | Auto-generated CUID |
| Any other string | Raw SQL default expression, e.g. "nanoid()" |
String Validation
validation: {
required: true,
min: 3,
max: 200,
matches: /^[a-z]+$/,
matchesMessage: "Only lowercase letters allowed",
unique: true,
uniqueInArray: true,
requiredMessage: "Title is required",
trim: true,
lowercase: true,
uppercase: false,
}
Storage Configuration (File Uploads)
When a string property has storage, it becomes a file upload field. The stored value is the file path (or URL) in your storage provider.
avatar: {
name: "Avatar",
type: "string",
storage: {
storagePath: "avatars/{entityId}",
acceptedFiles: ["image/*"],
maxSize: 5 * 1024 * 1024,
fileName: "{rand}_{file.name}.{file.ext}",
metadata: { cacheControl: "max-age=31536000" },
imageResize: {
maxWidth: 400,
maxHeight: 400,
mode: "cover",
format: "webp",
quality: 80
},
previewUrl: (path) => `https://cdn.example.com/${path}`,
processFile: async (file) => { return file; },
postProcess: async (pathOrUrl) => { return pathOrUrl; }
}
}
| StorageConfig Option | Type | Default | Description |
|---|
storagePath | string | (ctx) => string | required | Upload destination path. Placeholders: {file}, {file.name}, {file.ext}, {rand}, {entityId}, {propertyKey}, {path} |
acceptedFiles | FileType[] | all | Allowed MIME types. E.g. ["image/*"], ["application/pdf"] |
maxSize | number | — | Max file size in bytes |
fileName | string | (ctx) => string | — | Custom filename. Same placeholders as storagePath |
metadata | Record<string, unknown> | — | Upload metadata (e.g. Firebase UploadMetadata) |
imageResize | ImageResize | — | Resize images before upload |
previewUrl | (fileName) => string | — | Custom preview URL builder |
processFile | (file: File) => Promise<File> | — | Transform file before upload |
postProcess | (pathOrUrl) => Promise<string> | — | Transform saved path/URL after upload |
includeBucketUrl | boolean | false | Include bucket URL in saved path |
storeUrl | boolean | false | Save download URL instead of storage path |
storageSource | string | undefined (default backend) | Named storage source key — routes uploads to a specific backend registered in the backend storage map or in storageSources on <Rebase>. Must match a StorageSourceDefinition.key. |
Per-Property Backend Binding
When using multiple storage backends, use storageSource to route a specific property's uploads to a named backend:
image: {
type: "string",
storage: {
storageSource: "firebase",
storagePath: "products/{entityId}",
acceptedFiles: ["image/*"],
}
}
ImageResize Options
| Option | Type | Default | Description |
|---|
maxWidth | number | — | Max width in pixels |
maxHeight | number | — | Max height in pixels |
mode | "contain" | "cover" | "contain" | Resize fitting mode |
format | "original" | "jpeg" | "png" | "webp" | "original" | Output format |
quality | number (0-100) | 80 | Quality for JPEG/WebP |
Number Properties
price: {
name: "Price",
type: "number",
validation: { required: true, min: 0, positive: true }
}
Number-Specific Options
| Option | Type | Default | Description |
|---|
columnType | "integer" | "real" | "double precision" | "numeric" | "bigint" | "serial" | "bigserial" | auto | Database column type |
isId | boolean | "manual" | "increment" | string | — | Mark as primary key |
enum | EnumValues | — | Dropdown values |
Number isId Strategies
| Value | Behavior |
|---|
true / "manual" | User-defined numeric ID |
"increment" | Auto-incrementing integer (GENERATED BY DEFAULT AS IDENTITY) |
| Any other string | Raw SQL default expression |
Number Validation
validation: {
required: true,
min: 0,
max: 1000,
lessThan: 1001,
moreThan: -1,
positive: true,
negative: false,
integer: true,
unique: true
}
Boolean Properties
published: {
name: "Published",
type: "boolean",
defaultValue: false,
validation: { required: true }
}
No additional options beyond BaseProperty.
Date Properties
created_at: {
name: "Created At",
type: "date",
mode: "date_time",
autoValue: "on_create",
clearable: false,
admin: { readOnly: true }
}
updated_at: {
name: "Updated At",
type: "date",
mode: "date_time",
autoValue: "on_update",
admin: { readOnly: true }
}
Date-Specific Options
| Option | Type | Default | Description |
|---|
columnType | "timestamp" | "date" | "time" | "timestamp" | Database column type (with timezone) |
mode | "date" | "date_time" | "date_time" | Date-only or date + time picker |
autoValue | "on_create" | "on_update" | — | Auto-set timestamp on create or every update |
timezone | string | — | Timezone string for display |
clearable | boolean | false | Show clear button to set value to null |
Date Validation
validation: {
required: true,
min: new Date("2020-01-01"),
max: new Date("2030-12-31")
}
Map Properties (Nested Objects)
Maps store nested objects as JSONB in PostgreSQL. They can define their own inner properties schema.
address: {
name: "Address",
type: "map",
properties: {
street: { name: "Street", type: "string" },
city: { name: "City", type: "string", validation: { required: true } },
zip: { name: "ZIP Code", type: "string" },
country: { name: "Country", type: "string", enum: [
{ id: "US", label: "United States" },
{ id: "UK", label: "United Kingdom" }
]}
},
propertiesOrder: ["street", "city", "zip", "country"],
admin: { expanded: true, spreadChildren: true }
}
Map-Specific Options
| Option | Type | Default | Description |
|---|
columnType | "json" | "jsonb" | "jsonb" | Database column type |
properties | Properties | — | Nested property schema (same types as collection properties) |
admin.propertiesOrder | string[] | — | Display order of nested fields |
admin.previewProperties | string[] | — | Properties shown in preview/collapsed state |
keyValue | boolean | — | Render as key-value table with arbitrary keys (no properties needed) |
Map UI Options
| Option | Type | Default | Description |
|---|
ui.expanded | boolean | — | Expand map fields by default in forms |
ui.minimalistView | boolean | — | Compact rendering |
ui.spreadChildren | boolean | — | Spread child fields as if they were top-level form fields |
IMPORTANT FOR AGENTS: If validation.required is not set on the map property itself, an empty object {} is considered valid even if inner properties have required: true. Always set validation: { required: true } on the map if the entire object is mandatory.
Array Properties
Arrays can contain any element type (except nested arrays). They map to native Postgres arrays for primitives or JSONB for complex types.
tags: {
name: "Tags",
type: "array",
of: { type: "string" },
validation: { min: 1, max: 10 }
}
gallery: {
name: "Gallery",
type: "array",
of: {
type: "string",
storage: {
storagePath: "products/{entityId}/gallery",
acceptedFiles: ["image/*"]
}
}
}
metadata: {
name: "Metadata",
type: "array",
of: {
type: "map",
properties: {
key: { name: "Key", type: "string", validation: { required: true } },
value: { name: "Value", type: "string" }
}
},
admin: { expanded: true }
}
Array-Specific Options
| Option | Type | Default | Description |
|---|
columnType | "json" | "jsonb" | "text[]" | "integer[]" | "boolean[]" | "numeric[]" | auto | Database column type. Primitives default to native arrays |
of | Property | Property[] | — | Element type definition. Use a single Property for homogeneous arrays |
oneOf | { properties, propertiesOrder?, typeField?, valueField? } | — | Discriminated union for heterogeneous arrays (e.g. blog blocks) |
sortable | boolean | true | Allow drag-and-drop reordering |
canAddElements | boolean | true | Allow adding new elements |
Array UI Options
| Option | Type | Default | Description |
|---|
ui.expanded | boolean | — | Expand array items by default |
ui.minimalistView | boolean | — | Compact rendering |
admin.Field | ComponentRef | — | Custom field component for the entire array |
Array Validation
validation: {
required: true,
min: 1,
max: 20
}
oneOf (Discriminated Union Arrays)
Use oneOf for content blocks with different types (e.g. blog content):
content: {
name: "Content Blocks",
type: "array",
oneOf: {
typeField: "type",
valueField: "value",
properties: {
text: {
name: "Text Block",
type: "string",
admin: { markdown: true }
},
image: {
name: "Image",
type: "string",
storage: { storagePath: "blog/{entityId}/content" }
},
quote: {
name: "Quote",
type: "map",
properties: {
text: { name: "Quote Text", type: "string" },
author: { name: "Author", type: "string" }
}
}
}
}
}
Property Validation
Every property supports a validation object with these common options:
| Option | Type | Applies To | Description |
|---|
required | boolean | All | Field is mandatory |
requiredMessage | string | All | Custom error message when required validation fails |
unique | boolean | All | Value must be unique across all entities |
uniqueInArray | boolean | All | Value must be unique within parent array |
min | number | Date | String (length), Number, Date, Array (count) | Minimum value/length/count/date |
max | number | Date | String (length), Number, Date, Array (count) | Maximum value/length/count/date |
matches | string | RegExp | String | Regex pattern |
matchesMessage | string | String | Error message for regex mismatch |
trim | boolean | String | Trim whitespace before validation |
lowercase | boolean | String | Transform to lowercase |
uppercase | boolean | String | Transform to uppercase |
length | number | String | Exact string length |
lessThan | number | Number | Value must be less than |
moreThan | number | Number | Value must be greater than |
positive | boolean | Number | Value must be positive |
negative | boolean | Number | Value must be negative |
integer | boolean | Number | Value must be an integer |
Enum Values
Use the enum property on string or number types to define picklist options:
status: {
name: "Status",
type: "string",
defaultValue: "draft",
enum: [
{ id: "draft", label: "Draft", color: "gray" },
{ id: "published", label: "Published", color: "green" },
{ id: "archived", label: "Archived", color: "red", disabled: true }
]
}
Each EnumValueConfig entry supports:
| Field | Type | Description |
|---|
id | string | number | Stored value |
label | string | Display text |
color | ColorKey | ColorScheme | Optional chip color |
disabled | boolean | Option is visible but not selectable |
EnumValues can also be a Record<string, string | EnumValueConfig> for simpler definitions:
enum: {
draft: "Draft",
published: { label: "Published", color: "green" }
}
PostgreSQL Enum Database Constraints
When rebase schema generate is run, any string property configured with an enum array generates a corresponding PostgreSQL ENUM type (e.g. CREATE TYPE "public"."collection_property" AS ENUM('val1', 'val2')).
If you write custom server functions, backend callback handlers, or perform manual database updates:
- Validation Constraints: Pushing or inserting a value that is NOT defined in the collection's enum options array will trigger a PostgreSQL database-level check constraint error (e.g.,
invalid input value for enum).
- Adding Enum Options: Adding a new enum value in code requires generating and applying a database migration (e.g.,
pnpm db:generate followed by pnpm db:migrate or raw enum modification SQL). In development, using a raw SQL command to alter the enum may be necessary because modifying PostgreSQL enums dynamically can be restrictive.
Relations (Inline Property API)
Relations are defined directly on the property using type: "relation". The framework automatically extracts these into the collection's internal relations[] at normalization time — you do not need a separate relations[] array.
Many-to-One (Owning)
import { PostgresCollectionConfig } from "@rebasepro/types";
import authorsCollection from "./authors";
const postsCollection: PostgresCollectionConfig = {
name: "Posts",
slug: "posts",
table: "posts",
properties: {
author: {
name: "Author",
type: "relation",
relation: { kind: "belongsTo", target: () => authorsCollection }
}
}
};
This automatically creates an author_id foreign key column on the posts table.
Many-to-Many
tags: {
name: "Tags",
type: "relation",
relation: { kind: "manyToMany", target: () => tagsCollection }
}
This automatically creates a posts_tags junction table with post_id and tag_id columns.
One-to-Many
comments: {
name: "Comments",
type: "relation",
relation: {
kind: "hasMany",
target: () => commentsCollection,
foreignKeyOnTarget: "post_id"
}
}
Relation Property Options
| Option | Type | Default | Description |
|---|
The link goes under relation, and its kind decides which fields apply. | | | |
| Option | Type | Default | Description |
|---|
kind | "belongsTo" | "hasOne" | "hasMany" | "manyToMany" | "via" | — | Required. Which kind of link |
target | () => CollectionConfig | — | Target collection (a thunk, to survive circular imports) |
localKey | string | <relationName>_id | belongsTo only — column on THIS table |
foreignKeyOnTarget | string | <thisCollection>_id | hasOne/hasMany only — column on the TARGET's table |
through | { table?, sourceColumn?, targetColumn? } | derived | manyToMany only; sourceColumn names THIS collection |
joinPath | JoinStep[] | — | via only; read-only |
cardinality | "one" | "many" | — | via only — a join chain cannot imply it |
relationName | string | property key | The name it is addressed by: include, admin tab, nested path segment |
onDelete | OnAction | — | Cascade rule on delete |
onUpdate | OnAction | — | Cascade rule on update |
overrides | Partial<CollectionConfig> | — | Override target collection config when rendered as subcollection tab |
admin.fixedFilter | FilterValues | — | Filter applied when selecting related entities |
includeId | boolean | true | Show entity ID in the reference preview |
includeEntityLink | boolean | true | Show link to open the related entity |
isId | boolean | — | Mark as primary key |
validation | { required?: boolean } | — | Relation-level validation |
Relation UI Options
| Option | Type | Default | Description |
|---|
ui.widget | "select" | "dialog" | "select" | UI widget for selecting relations |
ui.previewProperties | string[] | — | Properties shown in relation preview (max 3) |
Cascade Rules (OnAction)
| Action | Behavior |
|---|
"cascade" | Propagate change to related rows |
"restrict" | Prevent if related rows exist |
"set null" | Set FK to NULL |
"no action" | Defer to constraint check |
"set default" | Set FK to default value |
Multi-Hop Joins (joinPath)
Use joinPath for advanced relationships that traverse multiple tables. When set, it overrides localKey, foreignKeyOnTarget, and through.
Each JoinStep defines one JOIN operation:
interface JoinStep {
table: string;
on: {
from: string | string[];
to: string | string[];
};
}
Example: Users → Permissions through Roles (4-table join)
permissions: {
name: "Permissions",
type: "relation",
relation: {
kind: "via",
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" }
}
]
}
}
Example: Composite key join
customer: {
name: "Customer",
type: "relation",
relation: {
kind: "via",
target: () => customersCollection,
cardinality: "one",
joinPath: [
{
table: "customers",
on: {
from: ["company_code", "region_id"],
to: ["code", "region_id"]
}
}
]
}
}
A via relation is read-only: a join chain does not say which row to write.
See full documentation: Relations
Collection Callbacks (Lifecycle Hooks)
IMPORTANT FOR AGENTS: Collections support lifecycle callbacks that let you run custom logic when entities are created, updated, read, or deleted. Use these to sync data between collections, transform data, validate business rules, or trigger side effects. Do NOT use raw SQL triggers, cron jobs, or external scripts when a callback can solve the problem.
Generic Type Parameters
CollectionCallbacks<M, USER> accepts two generic type parameters:
| Parameter | Default | Description |
|---|
M | Record<string, unknown> | Entity values type — maps to your collection's property schema |
USER | User | User type — extends the base User type with custom fields |
import { PostgresCollectionConfig, CollectionCallbacks } from "@rebasepro/types";
type Product = {
name: string;
price: number;
slug: string;
status: string;
};
const callbacks: CollectionCallbacks<Product> = {
beforeSave: async ({ values, status }) => {
if (values.name) {
values.slug = values.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
return values;
}
};
RebaseCallContext<USER>
All callbacks receive a context property of type RebaseCallContext<USER>. This is the subset of the full RebaseContext that is available in both frontend and backend (server-side) execution:
| Property | Type | Description |
|---|
context.client | RebaseClient | Invoke backend functions, access APIs |
context.data | RebaseData | Unified data access — context.data.products.create(...) |
context.storageSource | StorageSource | File storage operations |
context.user | USER | undefined | Authenticated user (set by backend in server-side callbacks) |
Reserved context.user Values
The context.user object is populated by the auth middleware. In server-side callbacks, it contains one of these reserved identities:
| Caller | context.user.uid | context.user.roles |
|---|
| JWT-authenticated end-user | Real user ID (e.g. "abc123") | Their assigned roles (e.g. ["viewer"]) |
Server-side rebase.data (cron jobs, custom functions) | "service" | ["admin"] |
| API key (default) | "api-key:{id}" | ["service"] |
| API key (admin) | "api-key:{id}" | ["admin", "service"] |
Anonymous (no auth, requireAuth: false) | "anon" | ["anon"] |
| Anonymous REST (no token) | undefined | N/A — context.user is not set; only the DataDriver is scoped |
IMPORTANT FOR AGENTS: rebase.data calls (used in cron jobs, afterSave side-effects, custom functions) go through the full middleware pipeline with the service key, so callbacks see uid: "service", roles: ["admin"]. Use this to gate behavior — e.g., skip PII masking for admin/service reads:
afterRead: async ({ row, context }) => {
if (context.user?.roles?.includes("admin")) return row;
return { ...row, email: "***@***.***" };
}
WARNING FOR AGENTS: Do NOT confuse RebaseCallContext (available in callbacks, both client & server) with RebaseContext (full context available only on the frontend, includes authController, snackbarController, sideEntityController, etc.). Entity callbacks always receive RebaseCallContext.
Callback Example
const jobSubmissionsCollection: PostgresCollectionConfig<{
title: string;
slug: string;
status: string;
description: string;
company_id: string;
created_at: string;
}> = {
name: "Job Submissions",
slug: "job_submissions",
table: "job_submissions",
callbacks: {
beforeSave: async ({ values, status }) => {
if (values.title) {
values.slug = values.title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
if (status === "new") {
values.created_at = new Date().toISOString();
}
return values;
},
afterSave: async ({ values, id, previousValues, context }) => {
if (values.status === "approved" && previousValues?.status !== "approved") {
await context.data.collection<Record<string, unknown>>("jobs").create({
title: values.title,
description: values.description,
company_id: values.company_id,
status: "published",
source_submission_id: id,
});
}
},
beforeDelete: async ({ row }) => {
if (row.status === "published") {
throw new Error("Cannot delete published submissions");
}
},
afterDelete: async ({ id, context }) => {
console.log(`Submission ${id} deleted`);
},
afterRead: async ({ row }) => {
return {
...row,
displayName: `${row.title} (${row.company_name})`
};
}
},
properties: { }
};
Available Callbacks
| Callback | When It Runs | Return Value | Can Block? |
|---|
beforeSave | Before write to DB (after validation) | Modified values (Partial<EntityValues<M>>) | Yes (throw to block) |
afterSave | After successful write | void | No |
afterSaveError | After a failed write | void | No |
afterRead | After reading from DB | Modified row (Record<string, unknown>) | No |
beforeDelete | Before deletion | void | boolean | Yes (throw to block) |
afterDelete | After successful deletion | void | No |
Callback Props Reference
beforeSave / afterSave / afterSaveError Props:
| Prop | Type | Description |
|---|
values | Partial<EntityValues<M>> | Entity values being saved |
id | string | number (optional in beforeSave) | Entity ID (undefined for new entities in beforeSave) |
previousValues | Partial<EntityValues<M>> | undefined | Previous values (for updates) |
status | EntityStatus | "new", "existing", or "copy" |
collection | CollectionConfig<M> | The collection definition |
path | string | Collection path |
context | RebaseCallContext<USER> | Context with client, data, storageSource, user |
afterRead Props:
| Prop | Type | Description |
|---|
row | Record<string, unknown> | The fetched row (flat — { id, ...columns }) |
collection | CollectionConfig<M> | The collection definition |
path | string | Collection path |
context | RebaseCallContext<USER> | Context |
beforeDelete / afterDelete Props:
| Prop | Type | Description |
|---|
row | Record<string, unknown> | The row being deleted (flat — { id, ...columns }) |
id | string | number | Entity ID |
collection | CollectionConfig<M> | The collection definition |
path | string | Collection path |
context | RebaseCallContext<USER> | Context |
Property-Level Callbacks
Individual properties also support callbacks with afterRead and beforeSave:
title: {
name: "Title",
type: "string",
callbacks: {
beforeSave: async ({ value, values }) => {
return value?.trim().replace(/\s+/g, " ");
},
afterRead: async ({ value }) => {
return value?.toUpperCase();
}
}
}
Common Use Cases
- Syncing data between collections — Use
afterSave to copy/move entities from one collection to another (e.g., approved submissions → published jobs)
- Computed fields — Use
beforeSave to generate slugs, timestamps, or derived values
- Validation — Use
beforeSave to enforce business rules beyond schema validation
- Notifications — Use
afterSave to send emails, Slack messages, or webhook calls
- Cascade operations — Use
afterDelete to clean up related records in other collections
- Data enrichment — Use
afterRead to add computed/virtual fields for display
See full documentation: Collection Callbacks
Entity Actions (Custom UI Buttons)
IMPORTANT FOR AGENTS: Collections support custom action buttons that appear in the collection table view and entity form. Use these for workflow actions like "Approve", "Send Email", "Export PDF", "Clone to Staging", etc. Do NOT build separate pages or scripts for common admin actions.
Add an entityActions array to any collection definition:
const jobSubmissionsCollection: PostgresCollectionConfig = {
name: "Job Submissions",
slug: "job_submissions",
table: "job_submissions",
properties: { },
admin: {
entityActions: [
{
name: "Approve",
icon: <CheckCircleIcon />,
isEnabled: ({ entity }) => entity?.values.status === "pending",
onClick: async ({ entity, context, onCollectionChange }) => {
if (!entity) return;
await context.data.collection<Record<string, unknown>>("job_submissions").update(entity.id, {
status: "approved"
});
context.snackbarController?.open({
type: "success",
message: "Submission approved!"
});
onCollectionChange?.();
}
},
{
name: "Export PDF",
collapsed: true,
includeInForm: true,
onClick: async ({ entity }) => {
window.open(`/api/functions/export-pdf/${entity?.id}`);
}
}
]
}
};
EntityAction Interface
| Property | Type | Default | Description |
|---|
name | string | — | Button label |
key | string | — | Override default actions: "edit", "delete", "copy" |
admin.icon | ReactElement | — | Optional icon |
onClick | (props: EntityActionClickProps) => void | Promise<void> | — | Action handler |
isEnabled | (props: EntityActionClickProps) => boolean | — | Conditionally disable the action |
collapsed | boolean | true | If true, show in overflow menu |
includeInForm | boolean | true | Show in entity form view |
showActionsInListView | boolean | false | Show inline on each row in list view |
EntityActionClickProps
The onClick and isEnabled handlers receive:
| Prop | Type | Description |
|---|
entity | Entity<M> | undefined | The current entity |
context | RebaseContext<USER> | Full context (includes snackbarController, authController, etc.) |
path | string | undefined | Collection path |
collection | CollectionConfig<M> | undefined | Collection definition |
formContext | FormContext | undefined | Form state (when called from a form) |
sideEntityController | SidePanelController | undefined | Side panel control |
admin.selectionController | SelectionController | undefined | Multi-select state (collection view) |
view | "collection" | "form" | Where the action was triggered |
admin.openEntityMode | "side_panel" | "full_screen" | "split" | "dialog" | How the entity form is opened |
highlightEntity | (entity) => void | Highlight a entity row |
unhighlightEntity | (entity) => void | Remove highlight |
navigateBack | () => void | Navigate back (e.g., after deleting) |
onCollectionChange | () => void | Refresh the collection view |
Entity Custom Views (Tabs)
Collections support entityViews — custom React components that appear as tabs in the entity detail view. Use these for previews, analytics, related items, or any custom UI per entity.
Entity views can be registered:
- Globally in the
<RebaseAdmin> component via the entityViews prop
- Per-collection by referencing view keys in the collection's
entityViews array
const entityViews = [
{
key: "blog_preview",
name: "Preview",
Builder: BlogEntryPreview,
position: "start" as const
}
];
<RebaseAdmin collections={collections} entityViews={entityViews}/>
const postsCollection: PostgresCollectionConfig = {
name: "Posts",
slug: "posts",
table: "posts",
properties: { },
admin: {
entityViews: ["blog_preview"]
}
};
The Builder component receives:
entity — The saved entity (may be undefined for new entities)
modifiedValues — Current unsaved form values
formContext — Form state and methods
collection — The collection definition
TypeScript Strict Checks in Custom Views
Under strict TypeScript checks (strictNullChecks: true), since entity is typed as optional (entity?: Entity<M>), accessing entity.id or entity.values directly will cause compilation errors like:
error TS18048: 'entity' is possibly 'undefined'.
Always add a guard clause at the very beginning of your custom view component to handle the undefined state:
function MyCustomView({ entity }: { entity?: { id: string; values: Record<string, unknown> } }) {
if (!entity) {
return null;
}
}
This narrows the type of entity for the remainder of the component, allowing safe property access (e.g. entity.id, entity.values.field).
Entity Preview & Title Resolution
Title Property Selection
By default, the property used as the entity's display title (previews, headers) is resolved as follows:
- If
titleProperty is explicitly specified on the collection, it is used.
- If
propertiesOrder is explicitly defined on the collection, the first non-ID property that is either a relation or string type is chosen as the title key.
- If no
propertiesOrder is defined, the framework searches the properties in order and picks the first string type property.
Relation Previews in Tables
When propertiesOrder is explicitly set, relation properties are not automatically filtered out of the default preview columns (whereas they are excluded from unordered defaults to avoid slow join operations).
resolveTitleToString Utility
Rebase provides a resolveTitleToString(title: any): string helper to turn complex entity title values (including dates, arrays, or relation shapes like { __type: "relation", id, data: { values } }) into clean, renderable strings. It prioritizes common fields like name, title, label, and displayName from nested relation data, falling back to stringified IDs or JSON representations.
Collection-Scoped Component Overrides
You can override built-in UI components for a specific collection by adding a components map to its definition. This is a collection-level implementation of Docusaurus-style swizzling.
Only collection-scoped components can be overridden here. App-level components (such as Shell.AppBar or HomePage) must be overridden globally at the <Rebase> root.
import { PostgresCollectionConfig } from "@rebasepro/types";
const productsCollection: PostgresCollectionConfig = {
name: "Products",
slug: "products",
table: "products",
properties: { ... },
admin: {
components: {
"Collection.EmptyState": { Component: ProductCustomEmptyState },
"Entity.Form": {
Component: (({ OriginalComponent, ...props }: {
OriginalComponent: React.ComponentType<Record<string, unknown>>
}) => (
<div>
<div className="bg-amber-100 p-2 text-amber-800 text-sm">Editing Product</div>
<OriginalComponent {...props} />
</div>
)) as unknown as React.ComponentType<Record<string, unknown>>,
wrap: true
}
}
}
};
Collection-Scoped Overridable Components
| Component Key | Original Props | Description |
|---|
"Collection.View" | CollectionViewProps | The entire collection landing page |
"Collection.Table" | CollectionTableProps | The default table view |
"Collection.Card" | CollectionCardProps | The card view item wrapper |
"Collection.EmptyState" | CollectionEmptyStateProps | Displayed when a collection has no items |
"Collection.Actions" | CollectionActionsProps | Toolbar buttons above the table/cards |
"Entity.Form" | EntityFormProps | The detail form for creating/updating |
"Entity.FormActions" | EntityFormActionsProps | Form submission/cancel button bar |
"Entity.DetailView" | EntityDetailViewProps | Read-only detail view |
"Entity.SidePanel" | EntitySidePanelProps | The side panel container for form/detail |
"Entity.Preview" | EntityPreviewProps | Inline reference/relation chip preview |
"Entity.MissingReference" | MissingReferenceProps | Rendered when a referenced entity is deleted or missing |
Authentication Collection Configuration (auth)
You can mark a collection as an authentication collection by setting the auth property to true (shorthand for { enabled: true }) or providing an AuthCollectionConfig object.
This is the collection used for user credentials, password hashing, and user management. Rebase auto-injects required auth actions (like resetting passwords) and routes them through this config.
import { PostgresCollectionConfig } from "@rebasepro/types";
const customUsersCollection: PostgresCollectionConfig = {
name: "Members",
slug: "members",
table: "members",
auth: {
enabled: true,
onCreateUser: async (values, ctx) => {
const hash = await ctx.hashPassword("welcome123");
return {
values: { ...values, passwordHash: hash, emailVerified: true },
temporaryPassword: "welcome123"
};
},
onResetPassword: async (userId, ctx) => {
if (ctx.emailConfigured) {
const tempPassword = "reset_" + Math.random().toString(36).substring(2, 8);
const hash = await ctx.hashPassword(tempPassword);
return { temporaryPassword: tempPassword, invitationSent: false };
}
return { invitationSent: false };
},
actions: {
resetPassword: true
}
},
properties: { ... }
};
Vector Properties
For similarity search and AI embeddings:
embedding: {
name: "Embedding",
type: "vector",
dimensions: 1536
}
| Option | Type | Description |
|---|
dimensions | number | Required. Number of dimensions in the vector |
Geopoint Properties
location: {
name: "Location",
type: "geopoint"
}
Security Rules (RLS)
Collections support Row Level Security via the securityRules array. This generates PostgreSQL RLS policies:
const postsCollection: PostgresCollectionConfig = {
name: "Posts",
slug: "posts",
table: "posts",
securityRules: [
{ operation: "select", using: "{status} = 'published'" },
{ operations: ["select", "insert", "update"], ownerField: "author_id" },
{ operation: "delete", roles: ["admin"] }
],
properties: { }
};
Security Rule Types
Rules are a discriminated union — you must use exactly one of: ownerField, access: "public", raw using/withCheck, or roles-only. They cannot be mixed (enforced at the type level).
| Variant | Key Field | Generates |
|---|
OwnerSecurityRule | ownerField: "user_id" | USING (user_id = auth.uid()) |
PublicSecurityRule | access: "public" | USING (true) |
RawSQLSecurityRule | using: "..." | Custom USING/WITH CHECK clause |
RolesOnlySecurityRule | (none of the above) | Roles-only, no row filter |
Common Options (SecurityRuleBase)
| Option | Type | Default | Description |
|---|
name | string | auto-generated | Human-readable policy name (must be unique per table) |
operation | SecurityOperation | "all" | Single operation: "select", "insert", "update", "delete", "all" |
operations | SecurityOperation[] | — | Multiple operations (generates one policy per operation) |
mode | "permissive" | "restrictive" | "permissive" | Permissive rules are OR'd; restrictive are AND'd |
roles | string[] | — | Application-level roles (Rebase roles, NOT Postgres roles). Can be combined with any variant |
pgRoles | string[] | ["public"] | Advanced: native PostgreSQL database roles for the TO clause |
SQL Context Functions
In raw SQL expressions (using, withCheck), these functions are available:
auth.uid() — the current user's ID
auth.roles() — comma-separated app role IDs
auth.jwt() — full JWT claims as JSONB
{column_name} — resolves to table.column_name
See full documentation: Security Rules
Dynamic Properties (Conditions)
For declarative, JSON-serializable dynamic behavior, use conditions instead of dynamicProps:
discount_percentage: {
name: "Discount %",
type: "number",
conditions: {
hidden: { "!": { "var": "values.sale_enabled" } },
required: { "var": "values.sale_enabled" },
min: 0,
max: 100
}
}
Available condition fields: disabled, disabledMessage, clearOnDisabled, hidden, readOnly, required, requiredMessage, min, max, defaultValue, enumConditions, allowedEnumValues, excludedEnumValues, referencePath, referenceFilter, canAddElements, sortable, acceptedFiles, maxFileSize.
Schema Migration Workflow
After modifying collections, apply changes to the database:
rebase schema generate
rebase db push
rebase db generate
rebase db migrate
References