@rebasepro/cli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +20 -28
  2. package/dist/commands/init.d.ts +3 -0
  3. package/dist/commands/skills.d.ts +1 -0
  4. package/dist/index.cjs +1843 -1414
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +1620 -1257
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/utils/package-manager.d.ts +2 -0
  9. package/package.json +15 -13
  10. package/skills/rebase-api/SKILL.md +662 -0
  11. package/skills/rebase-api/references/.gitkeep +3 -0
  12. package/skills/rebase-auth/SKILL.md +1143 -0
  13. package/skills/rebase-auth/references/.gitkeep +3 -0
  14. package/skills/rebase-backend-postgres/SKILL.md +633 -0
  15. package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
  16. package/skills/rebase-basics/SKILL.md +749 -0
  17. package/skills/rebase-basics/references/.gitkeep +3 -0
  18. package/skills/rebase-collections/SKILL.md +1328 -0
  19. package/skills/rebase-collections/references/.gitkeep +3 -0
  20. package/skills/rebase-cron-jobs/SKILL.md +699 -0
  21. package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
  22. package/skills/rebase-custom-functions/SKILL.md +233 -0
  23. package/skills/rebase-deployment/SKILL.md +583 -0
  24. package/skills/rebase-deployment/references/.gitkeep +3 -0
  25. package/skills/rebase-design-language/SKILL.md +664 -0
  26. package/skills/rebase-email/SKILL.md +701 -0
  27. package/skills/rebase-email/references/.gitkeep +1 -0
  28. package/skills/rebase-entity-history/SKILL.md +485 -0
  29. package/skills/rebase-entity-history/references/.gitkeep +1 -0
  30. package/skills/rebase-local-env-setup/SKILL.md +189 -0
  31. package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
  32. package/skills/rebase-realtime/SKILL.md +755 -0
  33. package/skills/rebase-realtime/references/.gitkeep +3 -0
  34. package/skills/rebase-sdk/SKILL.md +594 -0
  35. package/skills/rebase-sdk/references/.gitkeep +0 -0
  36. package/skills/rebase-storage/SKILL.md +765 -0
  37. package/skills/rebase-storage/references/.gitkeep +3 -0
  38. package/skills/rebase-studio/SKILL.md +746 -0
  39. package/skills/rebase-studio/references/.gitkeep +3 -0
  40. package/skills/rebase-ui-components/SKILL.md +1411 -0
  41. package/skills/rebase-ui-components/references/.gitkeep +3 -0
  42. package/skills/rebase-webhooks/SKILL.md +623 -0
  43. package/skills/rebase-webhooks/references/.gitkeep +1 -0
  44. package/templates/template/AGENTS.md +2 -0
  45. package/templates/template/CLAUDE.md +2 -0
  46. package/templates/template/ai-instructions.md +6 -3
  47. package/templates/template/backend/drizzle.config.ts +8 -5
  48. package/templates/template/backend/package.json +1 -1
  49. package/templates/template/backend/src/env.ts +1 -1
  50. package/templates/template/backend/src/index.ts +9 -6
  51. package/templates/template/config/collections/posts.ts +1 -1
  52. package/templates/template/config/collections/presets/blank/index.ts +3 -0
  53. package/templates/template/config/collections/presets/ecommerce/categories.ts +38 -0
  54. package/templates/template/config/collections/presets/ecommerce/index.ts +6 -0
  55. package/templates/template/config/collections/presets/ecommerce/orders.ts +64 -0
  56. package/templates/template/config/collections/presets/ecommerce/products.ts +62 -0
  57. package/templates/template/config/collections/users.ts +7 -10
  58. package/templates/template/docker-compose.yml +1 -1
  59. package/templates/template/frontend/package.json +2 -2
  60. package/templates/template/frontend/src/App.tsx +1 -7
  61. package/templates/template/frontend/vite.config.ts +0 -1
  62. package/templates/template/package.json +7 -0
  63. package/dist/commands/cli.test.d.ts +0 -1
  64. package/dist/commands/dev.test.d.ts +0 -1
  65. package/dist/commands/init.test.d.ts +0 -1
  66. package/dist/utils/package-manager.test.d.ts +0 -1
  67. package/dist/utils/project.test.d.ts +0 -1
@@ -0,0 +1,1328 @@
1
+ ---
2
+ name: rebase-collections
3
+ 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.
4
+ ---
5
+
6
+ # Rebase Collections
7
+
8
+ 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.
9
+
10
+ ## Core Concepts
11
+
12
+ ### Collections
13
+
14
+ A collection is defined as a TypeScript object implementing the `PostgresCollection` interface from `@rebasepro/types`. Each collection maps to a database table (via the `table` property) and generates:
15
+ - Full CRUD REST endpoints at `/api/data/{slug}`
16
+ - Optional GraphQL queries and mutations
17
+ - Admin panel views (table, forms, cards, kanban, list)
18
+
19
+ ### Properties
20
+
21
+ Properties define the fields of your collection. Rebase supports these built-in property types:
22
+
23
+ | Type | Description | PostgreSQL Column |
24
+ |------|-------------|-------------------|
25
+ | `string` | Text fields, URLs, emails, markdown, file uploads | `VARCHAR` / `TEXT` |
26
+ | `number` | Integers and decimals | `INTEGER` / `DOUBLE PRECISION` |
27
+ | `boolean` | True/false toggles | `BOOLEAN` |
28
+ | `date` | Date and datetime values | `TIMESTAMP` |
29
+ | `map` | Nested objects (JSON) | `JSONB` |
30
+ | `array` | Lists of values | `JSONB` or native arrays |
31
+ | `relation` | Foreign key to another collection (SQL JOINs) | FK column or junction table |
32
+ | `reference` | Legacy FK reference by collection slug (Firestore-style) | `UUID` with FK |
33
+ | `geopoint` | Latitude/longitude pairs | `JSONB` |
34
+ | `vector` | Embedding vectors for similarity search | `VECTOR` |
35
+
36
+ ### Reference vs Relation
37
+
38
+ > **IMPORTANT FOR AGENTS:** Understand the difference between `reference` and `relation` — they are NOT interchangeable.
39
+
40
+ | Feature | `relation` (Recommended) | `reference` (Legacy) |
41
+ |---------|-------------------------|---------------------|
42
+ | Backend | SQL JOINs, FK constraints | Stores a collection path + entity ID |
43
+ | Cascade rules | `onDelete`, `onUpdate` | None |
44
+ | Junction tables | Yes (many-to-many) | No |
45
+ | Multi-hop joins | Yes (`joinPath`) | No |
46
+ | Inverse lookups | Yes (`direction: "inverse"`) | No |
47
+ | Where to use | **PostgresCollection** | FirebaseCollection or legacy |
48
+ | Stored value | FK column(s) managed by framework | `{ id, path }` object or string |
49
+
50
+ **Use `relation` for all new Postgres collections.** The `reference` type exists for backward compatibility with Firestore-style collections.
51
+
52
+ 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.
53
+
54
+ ### Schema-as-Code
55
+
56
+ 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.
57
+
58
+ ## Defining a Collection
59
+
60
+ ```typescript
61
+ import { PostgresCollection } from "@rebasepro/types";
62
+
63
+ const productsCollection: PostgresCollection = {
64
+ name: "Products",
65
+ singularName: "Product",
66
+ slug: "products",
67
+ table: "products",
68
+ icon: "ShoppingBag",
69
+ group: "E-Commerce",
70
+ description: "Product catalog with pricing and inventory",
71
+ history: true,
72
+ defaultViewMode: "table",
73
+ enabledViews: ["table", "cards"],
74
+ openEntityMode: "split",
75
+ inlineEditing: true,
76
+ exportable: true,
77
+ selectionEnabled: true,
78
+ properties: {
79
+ id: {
80
+ name: "ID",
81
+ type: "number",
82
+ isId: "increment"
83
+ },
84
+ name: {
85
+ name: "Product Name",
86
+ type: "string",
87
+ validation: { required: true }
88
+ },
89
+ price: {
90
+ name: "Price",
91
+ type: "number",
92
+ validation: { required: true, min: 0 }
93
+ },
94
+ description: {
95
+ name: "Description",
96
+ type: "string",
97
+ multiline: true
98
+ },
99
+ published: {
100
+ name: "Published",
101
+ type: "boolean",
102
+ defaultValue: false
103
+ },
104
+ created_at: {
105
+ name: "Created At",
106
+ type: "date",
107
+ mode: "date_time",
108
+ autoValue: "on_create",
109
+ ui: { readOnly: true, hideFromCollection: true }
110
+ },
111
+ category: {
112
+ name: "Category",
113
+ type: "string",
114
+ enum: [
115
+ { id: "electronics", label: "Electronics", color: "blue" },
116
+ { id: "clothing", label: "Clothing", color: "purple" },
117
+ { id: "books", label: "Books", color: "green" }
118
+ ]
119
+ }
120
+ },
121
+ propertiesOrder: [
122
+ "name", "price", "category", "description",
123
+ "published", "created_at"
124
+ ]
125
+ };
126
+
127
+ export default productsCollection;
128
+ ```
129
+
130
+ ### Collection Options
131
+
132
+ | Option | Type | Default | Description |
133
+ |--------|------|---------|-------------|
134
+ | `name` | `string` | — | Display name (plural). E.g. `"Products"` |
135
+ | `singularName` | `string` | — | Singular display name. E.g. `"Product"` |
136
+ | `slug` | `string` | — | URL slug for API and routing |
137
+ | `table` | `string` | — | PostgreSQL table name |
138
+ | `schema` | `string` | `"public"` | PostgreSQL schema name |
139
+ | `description` | `string` | — | Description shown in the UI (supports Markdown) |
140
+ | `icon` | `string \| ReactNode` | — | Lucide icon name or React element |
141
+ | `group` | `string` | `"Views"` | Sidebar group heading |
142
+ | `driver` | `"postgres" \| undefined` | `undefined` | Backend driver (Postgres is default) |
143
+ | `history` | `boolean` | `false` | Enable entity audit trail (requires history plugin) |
144
+ | `defaultViewMode` | `ViewMode` | `"table"` | Default view: `"table"`, `"cards"`, `"kanban"`, `"list"` |
145
+ | `enabledViews` | `ViewMode[]` | `["table","cards","kanban"]` | Enabled view modes |
146
+ | `openEntityMode` | `"split" \| "side_panel" \| "full_screen" \| "dialog"` | `"full_screen"` | How entities open when clicked |
147
+ | `defaultEntityAction` | `"edit" \| "view"` | `"edit"` | Click behavior: open form or read-only view |
148
+ | `kanban` | `{ columnProperty: string }` | — | Kanban column config (requires enum property) |
149
+ | `propertiesOrder` | `string[]` | — | Field display order in forms and table |
150
+ | `entityViews` | `(string \| EntityCustomView)[]` | — | Custom tabs on entity detail |
151
+ | `titleProperty` | `string` | first text prop | Property used as entity title in previews |
152
+ | `previewProperties` | `string[]` | — | Properties shown when this collection is referenced |
153
+ | `listProperties` | `string[]` | — | Columns to display in list view |
154
+ | `selectionEnabled` | `boolean` | — | Enable row selection checkboxes |
155
+ | `selectionController` | `SelectionController` | — | External selection state controller |
156
+ | `inlineEditing` | `boolean` | — | Allow inline editing in collection table view |
157
+ | `exportable` | `boolean \| ExportConfig` | — | Enable data export. `true` for default, or `ExportConfig` for custom fields |
158
+ | `pagination` | `boolean \| number` | `true` (50) | Enable pagination. Set a number to customize page size |
159
+ | `defaultSize` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | — | Default rendered row size |
160
+ | `fixedFilter` | `FilterValues` | — | Permanent filter that cannot be changed by users |
161
+ | `defaultFilter` | `FilterValues` | — | Initial filter (can be changed by users) |
162
+ | `filterPresets` | `FilterPreset[]` | — | Quick-access filter buttons in toolbar |
163
+ | `sort` | `[string, "asc" \| "desc"]` | — | Default sort order. E.g. `["created_at", "desc"]` |
164
+ | `orderProperty` | `string` | — | Property key for drag-and-drop ordering (Kanban/general) |
165
+ | `formAutoSave` | `boolean` | `false` | Auto-save form on field change |
166
+ | `formView` | `FormViewConfig` | — | Custom component replacing the default entity form |
167
+ | `hideFromNavigation` | `boolean` | `false` | Hide from sidebar (still accessible via URL) |
168
+ | `hideIdFromForm` | `boolean` | `false` | Hide ID field in entity form |
169
+ | `hideIdFromCollection` | `boolean` | `false` | Hide ID column in collection table |
170
+ | `defaultSelectedView` | `string \| Function` | — | Auto-open a custom view/subcollection tab |
171
+ | `sideDialogWidth` | `number \| string` | — | Width of side dialog in pixels |
172
+ | `alwaysApplyDefaultValues` | `boolean` | `false` | Re-apply defaults on every update |
173
+ | `includeJsonView` | `boolean` | `false` | Show a JSON tab in entity detail |
174
+ | `localChangesBackup` | `"manual_apply" \| "auto_apply" \| false` | `"manual_apply"` | Local changes backup strategy |
175
+ | `disableDefaultActions` | `("edit" \| "copy" \| "delete")[]` | — | Disable built-in actions |
176
+ | `additionalFields` | `AdditionalFieldDelegate[]` | — | Virtual computed columns for views |
177
+ | `entityActions` | `EntityAction[]` | — | Custom action buttons (see Entity Actions section) |
178
+ | `Actions` | `ComponentRef[]` | — | Custom toolbar action components |
179
+ | `callbacks` | `EntityCallbacks<M, USER>` | — | Lifecycle hooks (see Entity Callbacks section) |
180
+ | `relations` | `Relation[]` | — | Explicit relation definitions (usually auto-extracted from properties) |
181
+ | `securityRules` | `SecurityRule[]` | — | Row Level Security policies |
182
+ | `childCollections` | `() => EntityCollection[]` | — | Nested child collections (populated automatically) |
183
+ | `overrides` | `EntityOverrides` | — | Override data source or storage source |
184
+ | `ownerId` | `string` | — | Owner user ID (for plugins/custom code) |
185
+ | `auth` | `boolean | AuthCollectionConfig` | — | Mark collection as authentication collection (user management, reset password, etc.) |
186
+ | `components` | `CollectionComponentOverrideMap` | — | Collection-scoped UI component overrides |
187
+
188
+
189
+ ## Common Property Options (BaseProperty)
190
+
191
+ All property types share these base options:
192
+
193
+ | Option | Type | Default | Description |
194
+ |--------|------|---------|-------------|
195
+ | `name` | `string` | — | Display label for the field |
196
+ | `description` | `string` | — | Help text displayed under the field |
197
+ | `columnName` | `string` | auto from key | Explicit DB column name (bypasses snake_case conversion) |
198
+ | `validation` | `PropertyValidationSchema` | — | Validation rules (see below) |
199
+ | `defaultValue` | `unknown` | — | Default value for new entities |
200
+ | `propertyConfig` | `string` | — | Reuse a globally defined property config by key |
201
+ | `dynamicProps` | `(props) => Partial<Property>` | — | Dynamic property overrides based on entity values |
202
+ | `conditions` | `PropertyConditions` | — | JSON Logic-based declarative conditions |
203
+ | `callbacks` | `PropertyCallbacks` | — | Per-field `afterRead` and `beforeSave` hooks |
204
+
205
+ ### UI Options (BaseUIConfig)
206
+
207
+ All property types support a `ui` object for display configuration:
208
+
209
+ | Option | Type | Default | Description |
210
+ |--------|------|---------|-------------|
211
+ | `ui.columnWidth` | `number` | — | Column width in pixels (table view) |
212
+ | `ui.hideFromCollection` | `boolean` | — | Hide from collection table view |
213
+ | `ui.readOnly` | `boolean` | — | Render as read-only preview |
214
+ | `ui.disabled` | `boolean \| PropertyDisabledConfig` | — | Disable editing |
215
+ | `ui.widthPercentage` | `number` | — | Width as percentage of form |
216
+ | `ui.customProps` | `unknown` | — | Custom props passed to the field component |
217
+ | `ui.Field` | `ComponentRef` | — | Custom field component |
218
+ | `ui.Preview` | `ComponentRef` | — | Custom preview/cell component |
219
+
220
+ ## String Properties
221
+
222
+ ```typescript
223
+ title: {
224
+ name: "Title",
225
+ type: "string",
226
+ validation: { required: true, min: 3, max: 200 },
227
+ multiline: false
228
+ }
229
+ ```
230
+
231
+ ### String-Specific Options
232
+
233
+ | Option | Type | Default | Description |
234
+ |--------|------|---------|-------------|
235
+ | `columnType` | `"varchar" \| "text" \| "char" \| "uuid"` | `"varchar"` | Database column type |
236
+ | `isId` | `boolean \| "manual" \| "uuid" \| "cuid" \| string` | — | Mark as primary key with generation strategy |
237
+ | `enum` | `EnumValues` | — | Dropdown/picklist values |
238
+ | `multiline` | `boolean` | `false` | Multi-line text area |
239
+ | `markdown` | `boolean` | `false` | Markdown editor with preview |
240
+ | `url` | `boolean \| PreviewType` | — | Render as link. `PreviewType`: `"image"`, `"video"`, `"audio"`, `"file"` |
241
+ | `email` | `boolean` | — | Email field rendering |
242
+ | `storage` | `StorageConfig` | — | File upload configuration (see Storage section) |
243
+ | `userSelect` | `boolean` | — | Render as user picker (value = user ID) |
244
+ | `previewAsTag` | `boolean` | — | Render value as a colored tag/chip |
245
+ | `reference` | `ReferenceProperty` | — | Lightweight reference to another collection by ID |
246
+
247
+ ### String isId Strategies
248
+
249
+ | Value | Behavior |
250
+ |-------|----------|
251
+ | `true` / `"manual"` | User-defined ID, must be entered manually |
252
+ | `"uuid"` | Auto-generated UUID via `gen_random_uuid()` |
253
+ | `"cuid"` | Auto-generated CUID |
254
+ | Any other string | Raw SQL default expression, e.g. `"nanoid()"` |
255
+
256
+ ### String Validation
257
+
258
+ ```typescript
259
+ validation: {
260
+ required: true,
261
+ min: 3, // Minimum string length
262
+ max: 200, // Maximum string length
263
+ matches: /^[a-z]+$/, // Regex pattern (string or RegExp)
264
+ matchesMessage: "Only lowercase letters allowed",
265
+ unique: true,
266
+ uniqueInArray: true,
267
+ requiredMessage: "Title is required",
268
+ trim: true, // Trim whitespace before validation
269
+ lowercase: true, // Transform to lowercase before validation
270
+ uppercase: false, // Transform to uppercase before validation
271
+ }
272
+ ```
273
+
274
+ ### Storage Configuration (File Uploads)
275
+
276
+ 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.
277
+
278
+ ```typescript
279
+ avatar: {
280
+ name: "Avatar",
281
+ type: "string",
282
+ storage: {
283
+ storagePath: "avatars/{entityId}",
284
+ acceptedFiles: ["image/*"],
285
+ maxSize: 5 * 1024 * 1024, // 5MB
286
+ fileName: "{rand}_{file.name}.{file.ext}",
287
+ metadata: { cacheControl: "max-age=31536000" },
288
+ imageResize: {
289
+ maxWidth: 400,
290
+ maxHeight: 400,
291
+ mode: "cover",
292
+ format: "webp",
293
+ quality: 80
294
+ },
295
+ previewUrl: (path) => `https://cdn.example.com/${path}`,
296
+ processFile: async (file) => { /* transform before upload */ return file; },
297
+ postProcess: async (pathOrUrl) => { /* transform saved value */ return pathOrUrl; }
298
+ }
299
+ }
300
+ ```
301
+
302
+ | StorageConfig Option | Type | Default | Description |
303
+ |---------------------|------|---------|-------------|
304
+ | `storagePath` | `string \| (ctx) => string` | **required** | Upload destination path. Placeholders: `{file}`, `{file.name}`, `{file.ext}`, `{rand}`, `{entityId}`, `{propertyKey}`, `{path}` |
305
+ | `acceptedFiles` | `FileType[]` | all | Allowed MIME types. E.g. `["image/*"]`, `["application/pdf"]` |
306
+ | `maxSize` | `number` | — | Max file size in bytes |
307
+ | `fileName` | `string \| (ctx) => string` | — | Custom filename. Same placeholders as `storagePath` |
308
+ | `metadata` | `Record<string, unknown>` | — | Upload metadata (e.g. Firebase `UploadMetadata`) |
309
+ | `imageResize` | `ImageResize` | — | Resize images before upload |
310
+ | `previewUrl` | `(fileName) => string` | — | Custom preview URL builder |
311
+ | `processFile` | `(file: File) => Promise<File>` | — | Transform file before upload |
312
+ | `postProcess` | `(pathOrUrl) => Promise<string>` | — | Transform saved path/URL after upload |
313
+ | `includeBucketUrl` | `boolean` | `false` | Include bucket URL in saved path |
314
+ | `storeUrl` | `boolean` | `false` | Save download URL instead of storage path |
315
+
316
+ ### ImageResize Options
317
+
318
+ | Option | Type | Default | Description |
319
+ |--------|------|---------|-------------|
320
+ | `maxWidth` | `number` | — | Max width in pixels |
321
+ | `maxHeight` | `number` | — | Max height in pixels |
322
+ | `mode` | `"contain" \| "cover"` | `"contain"` | Resize fitting mode |
323
+ | `format` | `"original" \| "jpeg" \| "png" \| "webp"` | `"original"` | Output format |
324
+ | `quality` | `number` (0-100) | `80` | Quality for JPEG/WebP |
325
+
326
+ ## Number Properties
327
+
328
+ ```typescript
329
+ price: {
330
+ name: "Price",
331
+ type: "number",
332
+ validation: { required: true, min: 0, positive: true }
333
+ }
334
+ ```
335
+
336
+ ### Number-Specific Options
337
+
338
+ | Option | Type | Default | Description |
339
+ |--------|------|---------|-------------|
340
+ | `columnType` | `"integer" \| "real" \| "double precision" \| "numeric" \| "bigint" \| "serial" \| "bigserial"` | auto | Database column type |
341
+ | `isId` | `boolean \| "manual" \| "increment" \| string` | — | Mark as primary key |
342
+ | `enum` | `EnumValues` | — | Dropdown values |
343
+
344
+ ### Number isId Strategies
345
+
346
+ | Value | Behavior |
347
+ |-------|----------|
348
+ | `true` / `"manual"` | User-defined numeric ID |
349
+ | `"increment"` | Auto-incrementing integer (`GENERATED BY DEFAULT AS IDENTITY`) |
350
+ | Any other string | Raw SQL default expression |
351
+
352
+ ### Number Validation
353
+
354
+ ```typescript
355
+ validation: {
356
+ required: true,
357
+ min: 0,
358
+ max: 1000,
359
+ lessThan: 1001,
360
+ moreThan: -1,
361
+ positive: true,
362
+ negative: false,
363
+ integer: true,
364
+ unique: true
365
+ }
366
+ ```
367
+
368
+ ## Boolean Properties
369
+
370
+ ```typescript
371
+ published: {
372
+ name: "Published",
373
+ type: "boolean",
374
+ defaultValue: false,
375
+ validation: { required: true }
376
+ }
377
+ ```
378
+
379
+ No additional options beyond `BaseProperty`.
380
+
381
+ ## Date Properties
382
+
383
+ ```typescript
384
+ created_at: {
385
+ name: "Created At",
386
+ type: "date",
387
+ mode: "date_time",
388
+ autoValue: "on_create",
389
+ clearable: false,
390
+ ui: { readOnly: true }
391
+ }
392
+
393
+ updated_at: {
394
+ name: "Updated At",
395
+ type: "date",
396
+ mode: "date_time",
397
+ autoValue: "on_update",
398
+ ui: { readOnly: true }
399
+ }
400
+ ```
401
+
402
+ ### Date-Specific Options
403
+
404
+ | Option | Type | Default | Description |
405
+ |--------|------|---------|-------------|
406
+ | `columnType` | `"timestamp" \| "date" \| "time"` | `"timestamp"` | Database column type (with timezone) |
407
+ | `mode` | `"date" \| "date_time"` | `"date_time"` | Date-only or date + time picker |
408
+ | `autoValue` | `"on_create" \| "on_update"` | — | Auto-set timestamp on create or every update |
409
+ | `timezone` | `string` | — | Timezone string for display |
410
+ | `clearable` | `boolean` | `false` | Show clear button to set value to `null` |
411
+
412
+ ### Date Validation
413
+
414
+ ```typescript
415
+ validation: {
416
+ required: true,
417
+ min: new Date("2020-01-01"),
418
+ max: new Date("2030-12-31")
419
+ }
420
+ ```
421
+
422
+ ## Map Properties (Nested Objects)
423
+
424
+ Maps store nested objects as `JSONB` in PostgreSQL. They can define their own inner properties schema.
425
+
426
+ ```typescript
427
+ address: {
428
+ name: "Address",
429
+ type: "map",
430
+ properties: {
431
+ street: { name: "Street", type: "string" },
432
+ city: { name: "City", type: "string", validation: { required: true } },
433
+ zip: { name: "ZIP Code", type: "string" },
434
+ country: { name: "Country", type: "string", enum: [
435
+ { id: "US", label: "United States" },
436
+ { id: "UK", label: "United Kingdom" }
437
+ ]}
438
+ },
439
+ propertiesOrder: ["street", "city", "zip", "country"],
440
+ ui: { expanded: true, spreadChildren: true }
441
+ }
442
+ ```
443
+
444
+ ### Map-Specific Options
445
+
446
+ | Option | Type | Default | Description |
447
+ |--------|------|---------|-------------|
448
+ | `columnType` | `"json" \| "jsonb"` | `"jsonb"` | Database column type |
449
+ | `properties` | `Properties` | — | Nested property schema (same types as collection properties) |
450
+ | `propertiesOrder` | `string[]` | — | Display order of nested fields |
451
+ | `previewProperties` | `string[]` | — | Properties shown in preview/collapsed state |
452
+ | `keyValue` | `boolean` | — | Render as key-value table with arbitrary keys (no `properties` needed) |
453
+
454
+ ### Map UI Options
455
+
456
+ | Option | Type | Default | Description |
457
+ |--------|------|---------|-------------|
458
+ | `ui.expanded` | `boolean` | — | Expand map fields by default in forms |
459
+ | `ui.minimalistView` | `boolean` | — | Compact rendering |
460
+ | `ui.spreadChildren` | `boolean` | — | Spread child fields as if they were top-level form fields |
461
+
462
+ > **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.
463
+
464
+ ## Array Properties
465
+
466
+ Arrays can contain any element type (except nested arrays). They map to native Postgres arrays for primitives or `JSONB` for complex types.
467
+
468
+ ```typescript
469
+ tags: {
470
+ name: "Tags",
471
+ type: "array",
472
+ of: { type: "string" },
473
+ validation: { min: 1, max: 10 }
474
+ }
475
+
476
+ gallery: {
477
+ name: "Gallery",
478
+ type: "array",
479
+ of: {
480
+ type: "string",
481
+ storage: {
482
+ storagePath: "products/{entityId}/gallery",
483
+ acceptedFiles: ["image/*"]
484
+ }
485
+ }
486
+ }
487
+
488
+ metadata: {
489
+ name: "Metadata",
490
+ type: "array",
491
+ of: {
492
+ type: "map",
493
+ properties: {
494
+ key: { name: "Key", type: "string", validation: { required: true } },
495
+ value: { name: "Value", type: "string" }
496
+ }
497
+ },
498
+ ui: { expanded: true }
499
+ }
500
+ ```
501
+
502
+ ### Array-Specific Options
503
+
504
+ | Option | Type | Default | Description |
505
+ |--------|------|---------|-------------|
506
+ | `columnType` | `"json" \| "jsonb" \| "text[]" \| "integer[]" \| "boolean[]" \| "numeric[]"` | auto | Database column type. Primitives default to native arrays |
507
+ | `of` | `Property \| Property[]` | — | Element type definition. Use a single `Property` for homogeneous arrays |
508
+ | `oneOf` | `{ properties, propertiesOrder?, typeField?, valueField? }` | — | Discriminated union for heterogeneous arrays (e.g. blog blocks) |
509
+ | `sortable` | `boolean` | `true` | Allow drag-and-drop reordering |
510
+ | `canAddElements` | `boolean` | `true` | Allow adding new elements |
511
+
512
+ ### Array UI Options
513
+
514
+ | Option | Type | Default | Description |
515
+ |--------|------|---------|-------------|
516
+ | `ui.expanded` | `boolean` | — | Expand array items by default |
517
+ | `ui.minimalistView` | `boolean` | — | Compact rendering |
518
+ | `ui.Field` | `ComponentRef` | — | Custom field component for the entire array |
519
+
520
+ ### Array Validation
521
+
522
+ ```typescript
523
+ validation: {
524
+ required: true,
525
+ min: 1, // Minimum number of elements
526
+ max: 20 // Maximum number of elements
527
+ }
528
+ ```
529
+
530
+ ### oneOf (Discriminated Union Arrays)
531
+
532
+ Use `oneOf` for content blocks with different types (e.g. blog content):
533
+
534
+ ```typescript
535
+ content: {
536
+ name: "Content Blocks",
537
+ type: "array",
538
+ oneOf: {
539
+ typeField: "type", // default: "type"
540
+ valueField: "value", // default: "value"
541
+ properties: {
542
+ text: {
543
+ name: "Text Block",
544
+ type: "string",
545
+ markdown: true
546
+ },
547
+ image: {
548
+ name: "Image",
549
+ type: "string",
550
+ storage: { storagePath: "blog/{entityId}/content" }
551
+ },
552
+ quote: {
553
+ name: "Quote",
554
+ type: "map",
555
+ properties: {
556
+ text: { name: "Quote Text", type: "string" },
557
+ author: { name: "Author", type: "string" }
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
563
+ // Stored as: [{ type: "text", value: "# Hello" }, { type: "image", value: "path/to/img.jpg" }]
564
+ ```
565
+
566
+ ## Property Validation
567
+
568
+ Every property supports a `validation` object with these common options:
569
+
570
+ | Option | Type | Applies To | Description |
571
+ |--------|------|-----------|-------------|
572
+ | `required` | `boolean` | All | Field is mandatory |
573
+ | `requiredMessage` | `string` | All | Custom error message when required validation fails |
574
+ | `unique` | `boolean` | All | Value must be unique across all entities |
575
+ | `uniqueInArray` | `boolean` | All | Value must be unique within parent array |
576
+ | `min` | `number \| Date` | String (length), Number, Date, Array (count) | Minimum value/length/count/date |
577
+ | `max` | `number \| Date` | String (length), Number, Date, Array (count) | Maximum value/length/count/date |
578
+ | `matches` | `string \| RegExp` | String | Regex pattern |
579
+ | `matchesMessage` | `string` | String | Error message for regex mismatch |
580
+ | `trim` | `boolean` | String | Trim whitespace before validation |
581
+ | `lowercase` | `boolean` | String | Transform to lowercase |
582
+ | `uppercase` | `boolean` | String | Transform to uppercase |
583
+ | `length` | `number` | String | Exact string length |
584
+ | `lessThan` | `number` | Number | Value must be less than |
585
+ | `moreThan` | `number` | Number | Value must be greater than |
586
+ | `positive` | `boolean` | Number | Value must be positive |
587
+ | `negative` | `boolean` | Number | Value must be negative |
588
+ | `integer` | `boolean` | Number | Value must be an integer |
589
+
590
+ ## Enum Values
591
+
592
+ Use the `enum` property on `string` or `number` types to define picklist options:
593
+
594
+ ```typescript
595
+ status: {
596
+ name: "Status",
597
+ type: "string",
598
+ defaultValue: "draft",
599
+ enum: [
600
+ { id: "draft", label: "Draft", color: "gray" },
601
+ { id: "published", label: "Published", color: "green" },
602
+ { id: "archived", label: "Archived", color: "red", disabled: true }
603
+ ]
604
+ }
605
+ ```
606
+
607
+ Each `EnumValueConfig` entry supports:
608
+
609
+ | Field | Type | Description |
610
+ |-------|------|-------------|
611
+ | `id` | `string \| number` | Stored value |
612
+ | `label` | `string` | Display text |
613
+ | `color` | `ColorKey \| ColorScheme` | Optional chip color |
614
+ | `disabled` | `boolean` | Option is visible but not selectable |
615
+
616
+ `EnumValues` can also be a `Record<string, string | EnumValueConfig>` for simpler definitions:
617
+
618
+ ```typescript
619
+ enum: {
620
+ draft: "Draft",
621
+ published: { label: "Published", color: "green" }
622
+ }
623
+ ```
624
+
625
+ ### PostgreSQL Enum Database Constraints
626
+
627
+ 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')`).
628
+
629
+ If you write custom server functions, backend callback handlers, or perform manual database updates:
630
+ 1. **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`).
631
+ 2. **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.
632
+
633
+
634
+ ## Relations (Inline Property API)
635
+
636
+ 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.
637
+
638
+ ### Many-to-One (Owning)
639
+
640
+ ```typescript
641
+ import { PostgresCollection } from "@rebasepro/types";
642
+ import authorsCollection from "./authors";
643
+
644
+ const postsCollection: PostgresCollection = {
645
+ name: "Posts",
646
+ slug: "posts",
647
+ table: "posts",
648
+ properties: {
649
+ author: {
650
+ name: "Author",
651
+ type: "relation",
652
+ target: () => authorsCollection,
653
+ cardinality: "one",
654
+ direction: "owning"
655
+ }
656
+ }
657
+ };
658
+ ```
659
+
660
+ This automatically creates an `author_id` foreign key column on the `posts` table.
661
+
662
+ ### Many-to-Many (Owning)
663
+
664
+ ```typescript
665
+ tags: {
666
+ name: "Tags",
667
+ type: "relation",
668
+ target: () => tagsCollection,
669
+ cardinality: "many",
670
+ direction: "owning"
671
+ }
672
+ ```
673
+
674
+ This automatically creates a `posts_tags` junction table with `post_id` and `tag_id` columns.
675
+
676
+ ### One-to-Many (Inverse)
677
+
678
+ ```typescript
679
+ comments: {
680
+ name: "Comments",
681
+ type: "relation",
682
+ target: () => commentsCollection,
683
+ cardinality: "many",
684
+ direction: "inverse",
685
+ foreignKeyOnTarget: "post_id"
686
+ }
687
+ ```
688
+
689
+ ### Relation Property Options
690
+
691
+ | Option | Type | Default | Description |
692
+ |--------|------|---------|-------------|
693
+ | `target` | `string \| (() => EntityCollection \| string)` | — | Target collection (use a function for lazy resolution to avoid circular imports) |
694
+ | `cardinality` | `"one" \| "many"` | `"one"` | Whether this references one or many records |
695
+ | `direction` | `"owning" \| "inverse"` | `"owning"` | Which side owns the FK or junction table |
696
+ | `localKey` | `string` | auto-inferred | Column on THIS table storing the FK (e.g. `"author_id"`) |
697
+ | `foreignKeyOnTarget` | `string` | auto-inferred | Column on TARGET table storing the FK (for inverse) |
698
+ | `through` | `{ table, sourceColumn, targetColumn }` | auto-inferred | Junction table config for many-to-many |
699
+ | `joinPath` | `JoinStep[]` | — | Explicit multi-hop join path (overrides all other join config) |
700
+ | `relationName` | `string` | property key | Override the relation name (defaults to the property key) |
701
+ | `inverseRelationName` | `string` | — | Name of the corresponding relation on the target collection |
702
+ | `onDelete` | `OnAction` | — | Cascade rule on delete |
703
+ | `onUpdate` | `OnAction` | — | Cascade rule on update |
704
+ | `overrides` | `Partial<EntityCollection>` | — | Override target collection config when rendered as subcollection tab |
705
+ | `fixedFilter` | `FilterValues` | — | Filter applied when selecting related entities |
706
+ | `includeId` | `boolean` | `true` | Show entity ID in the reference preview |
707
+ | `includeEntityLink` | `boolean` | `true` | Show link to open the related entity |
708
+ | `isId` | `boolean` | — | Mark as primary key |
709
+ | `validation` | `{ required?: boolean }` | — | Relation-level validation |
710
+
711
+ ### Relation UI Options
712
+
713
+ | Option | Type | Default | Description |
714
+ |--------|------|---------|-------------|
715
+ | `ui.widget` | `"select" \| "dialog"` | `"select"` | UI widget for selecting relations |
716
+ | `ui.previewProperties` | `string[]` | — | Properties shown in relation preview (max 3) |
717
+
718
+ ### Cascade Rules (OnAction)
719
+
720
+ | Action | Behavior |
721
+ |--------|----------|
722
+ | `"cascade"` | Propagate change to related rows |
723
+ | `"restrict"` | Prevent if related rows exist |
724
+ | `"set null"` | Set FK to NULL |
725
+ | `"no action"` | Defer to constraint check |
726
+ | `"set default"` | Set FK to default value |
727
+
728
+ ### Multi-Hop Joins (joinPath)
729
+
730
+ Use `joinPath` for advanced relationships that traverse multiple tables. When set, it overrides `localKey`, `foreignKeyOnTarget`, and `through`.
731
+
732
+ Each `JoinStep` defines one JOIN operation:
733
+
734
+ ```typescript
735
+ interface JoinStep {
736
+ table: string; // Table to join TO
737
+ on: {
738
+ from: string | string[]; // Column(s) on the PREVIOUS table
739
+ to: string | string[]; // Column(s) on THIS table
740
+ };
741
+ }
742
+ ```
743
+
744
+ **Example: Users → Permissions through Roles (4-table join)**
745
+
746
+ ```typescript
747
+ permissions: {
748
+ name: "Permissions",
749
+ type: "relation",
750
+ target: () => permissionsCollection,
751
+ cardinality: "many",
752
+ joinPath: [
753
+ {
754
+ table: "user_roles",
755
+ on: { from: "id", to: "user_id" } // users.id = user_roles.user_id
756
+ },
757
+ {
758
+ table: "roles",
759
+ on: { from: "role_id", to: "id" } // user_roles.role_id = roles.id
760
+ },
761
+ {
762
+ table: "role_permissions",
763
+ on: { from: "id", to: "role_id" } // roles.id = role_permissions.role_id
764
+ },
765
+ {
766
+ table: "permissions",
767
+ on: { from: "permission_id", to: "id" } // role_permissions.permission_id = permissions.id
768
+ }
769
+ ]
770
+ }
771
+ ```
772
+
773
+ **Example: Composite key join**
774
+
775
+ ```typescript
776
+ customer: {
777
+ name: "Customer",
778
+ type: "relation",
779
+ target: () => customersCollection,
780
+ cardinality: "one",
781
+ joinPath: [
782
+ {
783
+ table: "customers",
784
+ on: {
785
+ from: ["company_code", "region_id"], // orders table columns
786
+ to: ["code", "region_id"] // customers table columns
787
+ }
788
+ }
789
+ ]
790
+ }
791
+ ```
792
+
793
+ > **See full documentation:** [Relations](https://rebase.pro/docs/collections/relations)
794
+
795
+ ## Entity Callbacks (Lifecycle Hooks)
796
+
797
+ > **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.
798
+
799
+ ### Generic Type Parameters
800
+
801
+ `EntityCallbacks<M, USER>` accepts two generic type parameters:
802
+
803
+ | Parameter | Default | Description |
804
+ |-----------|---------|-------------|
805
+ | `M` | `Record<string, unknown>` | Entity values type — maps to your collection's property schema |
806
+ | `USER` | `User` | User type — extends the base `User` type with custom fields |
807
+
808
+ ```typescript
809
+ import { PostgresCollection, EntityCallbacks } from "@rebasepro/types";
810
+
811
+ interface Product {
812
+ name: string;
813
+ price: number;
814
+ slug: string;
815
+ status: string;
816
+ }
817
+
818
+ const callbacks: EntityCallbacks<Product> = {
819
+ beforeSave: async ({ values, status }) => {
820
+ // `values` is typed as Partial<Product>
821
+ if (values.name) {
822
+ values.slug = values.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
823
+ }
824
+ return values;
825
+ }
826
+ };
827
+ ```
828
+
829
+ ### RebaseCallContext\<USER\>
830
+
831
+ 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:
832
+
833
+ | Property | Type | Description |
834
+ |----------|------|-------------|
835
+ | `context.client` | `RebaseClient` | Invoke backend functions, access APIs |
836
+ | `context.data` | `RebaseData` | Unified data access — `context.data.products.create(...)` |
837
+ | `context.storageSource` | `StorageSource` | File storage operations |
838
+ | `context.user` | `USER \| undefined` | Authenticated user (set by backend in server-side callbacks) |
839
+
840
+ > **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`.
841
+
842
+ ### Callback Example
843
+
844
+ ```typescript
845
+ const jobSubmissionsCollection: PostgresCollection = {
846
+ name: "Job Submissions",
847
+ slug: "job_submissions",
848
+ table: "job_submissions",
849
+ callbacks: {
850
+ // Runs BEFORE saving — transform or validate data
851
+ beforeSave: async ({ values, status }) => {
852
+ if (values.title) {
853
+ values.slug = values.title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
854
+ }
855
+ if (status === "new") {
856
+ values.created_at = new Date();
857
+ }
858
+ return values;
859
+ },
860
+
861
+ // Runs AFTER saving — trigger side effects, sync other collections
862
+ afterSave: async ({ values, entityId, previousValues, context }) => {
863
+ if (values.status === "approved" && previousValues?.status !== "approved") {
864
+ await context.data.jobs.create({
865
+ title: values.title,
866
+ description: values.description,
867
+ company_id: values.company_id,
868
+ status: "published",
869
+ source_submission_id: entityId,
870
+ });
871
+ }
872
+ },
873
+
874
+ // Runs BEFORE deleting — block or validate
875
+ beforeDelete: async ({ entity }) => {
876
+ if (entity.values.status === "published") {
877
+ throw new Error("Cannot delete published submissions");
878
+ }
879
+ },
880
+
881
+ // Runs AFTER deleting — cleanup related data
882
+ afterDelete: async ({ entityId, context }) => {
883
+ console.log(`Submission ${entityId} deleted`);
884
+ },
885
+
886
+ // Runs AFTER reading — transform for display
887
+ afterRead: async ({ entity }) => {
888
+ return {
889
+ ...entity,
890
+ values: {
891
+ ...entity.values,
892
+ displayName: `${entity.values.title} (${entity.values.company_name})`
893
+ }
894
+ };
895
+ }
896
+ },
897
+ properties: { /* ... */ }
898
+ };
899
+ ```
900
+
901
+ ### Available Callbacks
902
+
903
+ | Callback | When It Runs | Return Value | Can Block? |
904
+ |----------|-------------|--------------|------------|
905
+ | `beforeSave` | Before write to DB (after validation) | Modified `values` (`Partial<EntityValues<M>>`) | Yes (throw to block) |
906
+ | `afterSave` | After successful write | `void` | No |
907
+ | `afterSaveError` | After a failed write | `void` | No |
908
+ | `afterRead` | After reading from DB | Modified `entity` (`Entity<M>`) | No |
909
+ | `beforeDelete` | Before deletion | `void \| boolean` | Yes (throw to block) |
910
+ | `afterDelete` | After successful deletion | `void` | No |
911
+
912
+ ### Callback Props Reference
913
+
914
+ **`beforeSave` / `afterSave` / `afterSaveError` Props:**
915
+
916
+ | Prop | Type | Description |
917
+ |------|------|-------------|
918
+ | `values` | `Partial<EntityValues<M>>` | Entity values being saved |
919
+ | `entityId` | `string \| number` (optional in `beforeSave`) | Entity ID (`undefined` for new entities in `beforeSave`) |
920
+ | `previousValues` | `Partial<EntityValues<M>> \| undefined` | Previous values (for updates) |
921
+ | `status` | `EntityStatus` | `"new"`, `"existing"`, or `"copy"` |
922
+ | `collection` | `EntityCollection<M>` | The collection definition |
923
+ | `path` | `string` | Collection path |
924
+ | `context` | `RebaseCallContext<USER>` | Context with `client`, `data`, `storageSource`, `user` |
925
+
926
+ **`afterRead` Props:**
927
+
928
+ | Prop | Type | Description |
929
+ |------|------|-------------|
930
+ | `entity` | `Entity<M>` | The fetched entity |
931
+ | `collection` | `EntityCollection<M>` | The collection definition |
932
+ | `path` | `string` | Collection path |
933
+ | `context` | `RebaseCallContext<USER>` | Context |
934
+
935
+ **`beforeDelete` / `afterDelete` Props:**
936
+
937
+ | Prop | Type | Description |
938
+ |------|------|-------------|
939
+ | `entity` | `Entity<M>` | The entity being deleted |
940
+ | `entityId` | `string \| number` | Entity ID |
941
+ | `collection` | `EntityCollection<M>` | The collection definition |
942
+ | `path` | `string` | Collection path |
943
+ | `context` | `RebaseCallContext<USER>` | Context |
944
+
945
+ ### Property-Level Callbacks
946
+
947
+ Individual properties also support `callbacks` with `afterRead` and `beforeSave`:
948
+
949
+ ```typescript
950
+ title: {
951
+ name: "Title",
952
+ type: "string",
953
+ callbacks: {
954
+ beforeSave: async ({ value, values }) => {
955
+ // Transform just this property's value before saving
956
+ return value?.trim().replace(/\s+/g, " ");
957
+ },
958
+ afterRead: async ({ value }) => {
959
+ // Transform just this property's value after reading
960
+ return value?.toUpperCase();
961
+ }
962
+ }
963
+ }
964
+ ```
965
+
966
+ ### Common Use Cases
967
+
968
+ - **Syncing data between collections** — Use `afterSave` to copy/move entities from one collection to another (e.g., approved submissions → published jobs)
969
+ - **Computed fields** — Use `beforeSave` to generate slugs, timestamps, or derived values
970
+ - **Validation** — Use `beforeSave` to enforce business rules beyond schema validation
971
+ - **Notifications** — Use `afterSave` to send emails, Slack messages, or webhook calls
972
+ - **Cascade operations** — Use `afterDelete` to clean up related records in other collections
973
+ - **Data enrichment** — Use `afterRead` to add computed/virtual fields for display
974
+
975
+ > **See full documentation:** [Entity Callbacks](https://rebase.pro/docs/collections/callbacks)
976
+
977
+ ## Entity Actions (Custom UI Buttons)
978
+
979
+ > **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.
980
+
981
+ Add an `entityActions` array to any collection definition:
982
+
983
+ ```typescript
984
+ const jobSubmissionsCollection: PostgresCollection = {
985
+ name: "Job Submissions",
986
+ slug: "job_submissions",
987
+ table: "job_submissions",
988
+ entityActions: [
989
+ {
990
+ name: "Approve",
991
+ icon: <CheckCircleIcon />,
992
+ // Only show for pending submissions
993
+ isEnabled: ({ entity }) => entity?.values.status === "pending",
994
+ onClick: async ({ entity, context, onCollectionChange }) => {
995
+ if (!entity) return;
996
+ await context.data.job_submissions.update(entity.id, {
997
+ status: "approved"
998
+ });
999
+ context.snackbarController?.open({
1000
+ type: "success",
1001
+ message: "Submission approved!"
1002
+ });
1003
+ onCollectionChange?.();
1004
+ }
1005
+ },
1006
+ {
1007
+ name: "Export PDF",
1008
+ collapsed: true, // Show in overflow menu
1009
+ includeInForm: true,
1010
+ onClick: async ({ entity }) => {
1011
+ window.open(`/api/functions/export-pdf/${entity?.id}`);
1012
+ }
1013
+ }
1014
+ ],
1015
+ properties: { /* ... */ }
1016
+ };
1017
+ ```
1018
+
1019
+ ### EntityAction Interface
1020
+
1021
+ | Property | Type | Default | Description |
1022
+ |----------|------|---------|-------------|
1023
+ | `name` | `string` | — | Button label |
1024
+ | `key` | `string` | — | Override default actions: `"edit"`, `"delete"`, `"copy"` |
1025
+ | `icon` | `ReactElement` | — | Optional icon |
1026
+ | `onClick` | `(props: EntityActionClickProps) => void \| Promise<void>` | — | Action handler |
1027
+ | `isEnabled` | `(props: EntityActionClickProps) => boolean` | — | Conditionally disable the action |
1028
+ | `collapsed` | `boolean` | `true` | If `true`, show in overflow menu |
1029
+ | `includeInForm` | `boolean` | `true` | Show in entity form view |
1030
+ | `showActionsInListView` | `boolean` | `false` | Show inline on each row in list view |
1031
+
1032
+ ### EntityActionClickProps
1033
+
1034
+ The `onClick` and `isEnabled` handlers receive:
1035
+
1036
+ | Prop | Type | Description |
1037
+ |------|------|-------------|
1038
+ | `entity` | `Entity<M> \| undefined` | The current entity |
1039
+ | `context` | `RebaseContext<USER>` | Full context (includes `snackbarController`, `authController`, etc.) |
1040
+ | `path` | `string \| undefined` | Collection path |
1041
+ | `collection` | `EntityCollection<M> \| undefined` | Collection definition |
1042
+ | `formContext` | `FormContext \| undefined` | Form state (when called from a form) |
1043
+ | `sideEntityController` | `SideEntityController \| undefined` | Side panel control |
1044
+ | `selectionController` | `SelectionController \| undefined` | Multi-select state (collection view) |
1045
+ | `view` | `"collection" \| "form"` | Where the action was triggered |
1046
+ | `openEntityMode` | `"side_panel" \| "full_screen" \| "split" \| "dialog"` | How the entity form is opened |
1047
+ | `highlightEntity` | `(entity) => void` | Highlight an entity row |
1048
+ | `unhighlightEntity` | `(entity) => void` | Remove highlight |
1049
+ | `navigateBack` | `() => void` | Navigate back (e.g., after deleting) |
1050
+ | `onCollectionChange` | `() => void` | Refresh the collection view |
1051
+
1052
+ ## Entity Custom Views (Tabs)
1053
+
1054
+ 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.
1055
+
1056
+ Entity views can be registered:
1057
+ 1. **Globally** in the `<RebaseCMS>` component via the `entityViews` prop
1058
+ 2. **Per-collection** by referencing view keys in the collection's `entityViews` array
1059
+
1060
+ ```typescript
1061
+ // Global registration in App.tsx
1062
+ const entityViews = [
1063
+ {
1064
+ key: "blog_preview",
1065
+ name: "Preview",
1066
+ Builder: BlogEntryPreview,
1067
+ position: "start" as const
1068
+ }
1069
+ ];
1070
+
1071
+ <RebaseCMS collections={collections} entityViews={entityViews}/>
1072
+
1073
+ // Per-collection reference in collection definition
1074
+ const postsCollection: PostgresCollection = {
1075
+ name: "Posts",
1076
+ slug: "posts",
1077
+ table: "posts",
1078
+ entityViews: ["blog_preview"], // References the global view by key
1079
+ properties: { /* ... */ }
1080
+ };
1081
+ ```
1082
+
1083
+ The `Builder` component receives:
1084
+ - `entity` — The saved entity (may be `undefined` for new entities)
1085
+ - `modifiedValues` — Current unsaved form values
1086
+ - `formContext` — Form state and methods
1087
+ - `collection` — The collection definition
1088
+
1089
+ ### TypeScript Strict Checks in Custom Views
1090
+
1091
+ 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:
1092
+ `error TS18048: 'entity' is possibly 'undefined'.`
1093
+
1094
+ Always add a guard clause at the very beginning of your custom view component to handle the undefined state:
1095
+ ```typescript
1096
+ if (!entity) {
1097
+ return null; // or show a loading/error state
1098
+ }
1099
+ ```
1100
+ This narrows the type of `entity` for the remainder of the component, allowing safe property access (e.g. `entity.id`, `entity.values.field`).
1101
+
1102
+ ## Entity Preview & Title Resolution
1103
+
1104
+ ### Title Property Selection
1105
+ By default, the property used as the entity's display title (previews, headers) is resolved as follows:
1106
+ 1. If `titleProperty` is explicitly specified on the collection, it is used.
1107
+ 2. 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.
1108
+ 3. If no `propertiesOrder` is defined, the framework searches the properties in order and picks the first string type property.
1109
+
1110
+ ### Relation Previews in Tables
1111
+ 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).
1112
+
1113
+ ### resolveTitleToString Utility
1114
+ 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.
1115
+
1116
+ ## Collection-Scoped Component Overrides
1117
+
1118
+ 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.
1119
+
1120
+ 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.
1121
+
1122
+ ```typescript
1123
+ import { PostgresCollection } from "@rebasepro/types";
1124
+
1125
+ const productsCollection: PostgresCollection = {
1126
+ name: "Products",
1127
+ slug: "products",
1128
+ table: "products",
1129
+ components: {
1130
+ // Eject Mode: Replace the empty state view entirely
1131
+ "Collection.EmptyState": { Component: ProductCustomEmptyState },
1132
+
1133
+ // Wrap Mode: Wrap the built-in form, augmenting it
1134
+ "Entity.Form": {
1135
+ Component: ({ OriginalComponent, ...props }) => (
1136
+ <div>
1137
+ <div className="bg-amber-100 p-2 text-amber-800 text-sm">Editing Product</div>
1138
+ <OriginalComponent {...props} />
1139
+ </div>
1140
+ ),
1141
+ wrap: true
1142
+ }
1143
+ },
1144
+ properties: { ... }
1145
+ };
1146
+ ```
1147
+
1148
+ ### Collection-Scoped Overridable Components
1149
+
1150
+ | Component Key | Original Props | Description |
1151
+ |---|---|---|
1152
+ | `"Collection.View"` | `CollectionViewProps` | The entire collection landing page |
1153
+ | `"Collection.Table"` | `CollectionTableProps` | The default table view |
1154
+ | `"Collection.Card"` | `CollectionCardProps` | The card view item wrapper |
1155
+ | `"Collection.EmptyState"` | `CollectionEmptyStateProps` | Displayed when a collection has no items |
1156
+ | `"Collection.Actions"` | `CollectionActionsProps` | Toolbar buttons above the table/cards |
1157
+ | `"Entity.Form"` | `EntityFormProps` | The detail form for creating/updating |
1158
+ | `"Entity.FormActions"` | `EntityFormActionsProps` | Form submission/cancel button bar |
1159
+ | `"Entity.DetailView"` | `EntityDetailViewProps` | Read-only detail view |
1160
+ | `"Entity.SidePanel"` | `EntitySidePanelProps` | The side panel container for form/detail |
1161
+ | `"Entity.Preview"` | `EntityPreviewProps` | Inline reference/relation chip preview |
1162
+ | `"Entity.MissingReference"` | `MissingReferenceProps` | Rendered when a referenced entity is deleted or missing |
1163
+
1164
+ ## Authentication Collection Configuration (auth)
1165
+
1166
+ 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.
1167
+
1168
+ 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.
1169
+
1170
+ ```typescript
1171
+ import { PostgresCollection } from "@rebasepro/types";
1172
+
1173
+ const customUsersCollection: PostgresCollection = {
1174
+ name: "Members",
1175
+ slug: "members",
1176
+ table: "members",
1177
+ auth: {
1178
+ enabled: true,
1179
+ // Override default user creation flow
1180
+ onCreateUser: async (values, ctx) => {
1181
+ const hash = await ctx.hashPassword("welcome123");
1182
+ return {
1183
+ values: { ...values, passwordHash: hash, emailVerified: true },
1184
+ temporaryPassword: "welcome123"
1185
+ };
1186
+ },
1187
+ // Override default password reset flow
1188
+ onResetPassword: async (userId, ctx) => {
1189
+ if (ctx.emailConfigured) {
1190
+ const tempPassword = "reset_" + Math.random().toString(36).substring(2, 8);
1191
+ const hash = await ctx.hashPassword(tempPassword);
1192
+ // Custom email sending or persistence
1193
+ return { temporaryPassword: tempPassword, invitationSent: false };
1194
+ }
1195
+ return { invitationSent: false };
1196
+ },
1197
+ // Override/disable default user management actions
1198
+ actions: {
1199
+ resetPassword: true // Or false to disable, or a custom EntityAction to replace the UI
1200
+ }
1201
+ },
1202
+ properties: { ... }
1203
+ };
1204
+ ```
1205
+
1206
+ ## Vector Properties
1207
+
1208
+ For similarity search and AI embeddings:
1209
+
1210
+ ```typescript
1211
+ embedding: {
1212
+ name: "Embedding",
1213
+ type: "vector",
1214
+ dimensions: 1536 // Required — must match your model's output dimensions
1215
+ }
1216
+ ```
1217
+
1218
+ | Option | Type | Description |
1219
+ |--------|------|-------------|
1220
+ | `dimensions` | `number` | **Required.** Number of dimensions in the vector |
1221
+
1222
+ ## Geopoint Properties
1223
+
1224
+ ```typescript
1225
+ location: {
1226
+ name: "Location",
1227
+ type: "geopoint"
1228
+ }
1229
+ // Stored as JSONB: { latitude: number, longitude: number }
1230
+ ```
1231
+
1232
+ ## Security Rules (RLS)
1233
+
1234
+ Collections support **Row Level Security** via the `securityRules` array. This generates PostgreSQL RLS policies:
1235
+
1236
+ ```typescript
1237
+ const postsCollection: PostgresCollection = {
1238
+ name: "Posts",
1239
+ slug: "posts",
1240
+ table: "posts",
1241
+ securityRules: [
1242
+ // Anyone can read published posts
1243
+ { operation: "select", access: "public", using: "{status} = 'published'" },
1244
+ // Authors can see/edit their own
1245
+ { operations: ["select", "insert", "update"], ownerField: "author_id" },
1246
+ // Only admins can delete
1247
+ { operation: "delete", roles: ["admin"] }
1248
+ ],
1249
+ properties: { /* ... */ }
1250
+ };
1251
+ ```
1252
+
1253
+ ### Security Rule Types
1254
+
1255
+ 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).
1256
+
1257
+ | Variant | Key Field | Generates |
1258
+ |---------|-----------|-----------|
1259
+ | `OwnerSecurityRule` | `ownerField: "user_id"` | `USING (user_id = auth.uid())` |
1260
+ | `PublicSecurityRule` | `access: "public"` | `USING (true)` |
1261
+ | `RawSQLSecurityRule` | `using: "..."` | Custom USING/WITH CHECK clause |
1262
+ | `RolesOnlySecurityRule` | (none of the above) | Roles-only, no row filter |
1263
+
1264
+ ### Common Options (SecurityRuleBase)
1265
+
1266
+ | Option | Type | Default | Description |
1267
+ |--------|------|---------|-------------|
1268
+ | `name` | `string` | auto-generated | Human-readable policy name (must be unique per table) |
1269
+ | `operation` | `SecurityOperation` | `"all"` | Single operation: `"select"`, `"insert"`, `"update"`, `"delete"`, `"all"` |
1270
+ | `operations` | `SecurityOperation[]` | — | Multiple operations (generates one policy per operation) |
1271
+ | `mode` | `"permissive" \| "restrictive"` | `"permissive"` | Permissive rules are OR'd; restrictive are AND'd |
1272
+ | `roles` | `string[]` | — | Application-level roles (Rebase roles, NOT Postgres roles). Can be combined with any variant |
1273
+ | `pgRoles` | `string[]` | `["public"]` | Advanced: native PostgreSQL database roles for the `TO` clause |
1274
+
1275
+ ### SQL Context Functions
1276
+
1277
+ In raw SQL expressions (`using`, `withCheck`), these functions are available:
1278
+ - `auth.uid()` — the current user's ID
1279
+ - `auth.roles()` — comma-separated app role IDs
1280
+ - `auth.jwt()` — full JWT claims as JSONB
1281
+ - `{column_name}` — resolves to `table.column_name`
1282
+
1283
+ > **See full documentation:** [Security Rules](https://rebase.pro/docs/collections/security-rules)
1284
+
1285
+ ## Dynamic Properties (Conditions)
1286
+
1287
+ For declarative, JSON-serializable dynamic behavior, use `conditions` instead of `dynamicProps`:
1288
+
1289
+ ```typescript
1290
+ discount_percentage: {
1291
+ name: "Discount %",
1292
+ type: "number",
1293
+ conditions: {
1294
+ // Only show when sale_enabled is true
1295
+ hidden: { "!": { "var": "values.sale_enabled" } },
1296
+ // Required when visible
1297
+ required: { "var": "values.sale_enabled" },
1298
+ // Min 0, max 100
1299
+ min: 0,
1300
+ max: 100
1301
+ }
1302
+ }
1303
+ ```
1304
+
1305
+ Available condition fields: `disabled`, `disabledMessage`, `clearOnDisabled`, `hidden`, `readOnly`, `required`, `requiredMessage`, `min`, `max`, `defaultValue`, `enumConditions`, `allowedEnumValues`, `excludedEnumValues`, `referencePath`, `referenceFilter`, `canAddElements`, `sortable`, `acceptedFiles`, `maxFileSize`.
1306
+
1307
+ ## Schema Migration Workflow
1308
+
1309
+ After modifying collections, apply changes to the database:
1310
+
1311
+ ```bash
1312
+ # All commands run from the project root directory unless noted
1313
+
1314
+ # 1. Regenerate the Drizzle schema from your collection definitions
1315
+ rebase schema generate
1316
+
1317
+ # 2a. Development — push changes directly
1318
+ rebase db push
1319
+
1320
+ # 2b. Production — generate and review migration files
1321
+ rebase db generate
1322
+ rebase db migrate
1323
+ ```
1324
+
1325
+ ## References
1326
+
1327
+ - **Documentation:** [rebase.pro/docs](https://rebase.pro/docs)
1328
+ - **GitHub:** [github.com/rebasepro/rebase](https://github.com/rebasepro/rebase)