@rebasepro/agent-skills 0.7.0 → 0.9.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.
- package/LICENSE +21 -0
- package/package.json +9 -3
- package/skills/rebase-admin/SKILL.md +32 -32
- package/skills/rebase-api/SKILL.md +6 -6
- package/skills/rebase-auth/SKILL.md +52 -12
- package/skills/rebase-backend-postgres/SKILL.md +3 -3
- package/skills/rebase-basics/SKILL.md +11 -6
- package/skills/rebase-collections/SKILL.md +84 -46
- package/skills/rebase-cron-jobs/SKILL.md +5 -0
- package/skills/rebase-custom-functions/SKILL.md +21 -0
- package/skills/rebase-deployment/SKILL.md +12 -3
- package/skills/rebase-email/SKILL.md +1 -1
- package/skills/rebase-entity-history/SKILL.md +9 -9
- package/skills/rebase-realtime/SKILL.md +5 -5
- package/skills/rebase-sdk/SKILL.md +23 -0
- package/skills/rebase-security/SKILL.md +68 -28
- package/skills/rebase-storage/SKILL.md +180 -12
- package/skills/rebase-studio/SKILL.md +8 -8
- package/skills/rebase-ui-components/SKILL.md +91 -0
- package/skills/rebase-webhooks/SKILL.md +25 -30
|
@@ -11,7 +11,7 @@ Rebase collections are the core building blocks of your data model. They define
|
|
|
11
11
|
|
|
12
12
|
### Collections
|
|
13
13
|
|
|
14
|
-
A collection is defined as a TypeScript object implementing the `
|
|
14
|
+
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:
|
|
15
15
|
- Full CRUD REST endpoints at `/api/data/{slug}`
|
|
16
16
|
- Optional GraphQL queries and mutations
|
|
17
17
|
- Admin panel views (table, forms, cards, kanban, list)
|
|
@@ -44,7 +44,7 @@ Properties define the fields of your collection. Rebase supports these built-in
|
|
|
44
44
|
| Junction tables | Yes (many-to-many) | No |
|
|
45
45
|
| Multi-hop joins | Yes (`joinPath`) | No |
|
|
46
46
|
| Inverse lookups | Yes (`direction: "inverse"`) | No |
|
|
47
|
-
| Where to use | **
|
|
47
|
+
| Where to use | **PostgresCollectionConfig** | FirebaseCollectionConfig or legacy |
|
|
48
48
|
| Stored value | FK column(s) managed by framework | `{ id, path }` object or string |
|
|
49
49
|
|
|
50
50
|
**Use `relation` for all new Postgres collections.** The `reference` type exists for backward compatibility with Firestore-style collections.
|
|
@@ -58,9 +58,9 @@ Collections are defined as standalone TypeScript files under `config/collections
|
|
|
58
58
|
## Defining a Collection
|
|
59
59
|
|
|
60
60
|
```typescript
|
|
61
|
-
import {
|
|
61
|
+
import { PostgresCollectionConfig } from "@rebasepro/types";
|
|
62
62
|
|
|
63
|
-
const productsCollection:
|
|
63
|
+
const productsCollection: PostgresCollectionConfig = {
|
|
64
64
|
name: "Products",
|
|
65
65
|
singularName: "Product",
|
|
66
66
|
slug: "products",
|
|
@@ -178,10 +178,10 @@ export default productsCollection;
|
|
|
178
178
|
| `additionalFields` | `AdditionalFieldDelegate[]` | — | Virtual computed columns for views |
|
|
179
179
|
| `entityActions` | `EntityAction[]` | — | Custom action buttons (see Entity Actions section) |
|
|
180
180
|
| `Actions` | `ComponentRef[]` | — | Custom toolbar action components |
|
|
181
|
-
| `callbacks` | `
|
|
181
|
+
| `callbacks` | `CollectionCallbacks<M, USER>` | — | Lifecycle hooks (see Collection Callbacks section) |
|
|
182
182
|
| `relations` | `Relation[]` | — | Explicit relation definitions (usually auto-extracted from properties) |
|
|
183
183
|
| `securityRules` | `SecurityRule[]` | — | Row Level Security policies |
|
|
184
|
-
| `childCollections` | `() =>
|
|
184
|
+
| `childCollections` | `() => CollectionConfig[]` | — | Nested child collections (populated automatically) |
|
|
185
185
|
| `overrides` | `EntityOverrides` | — | Override data source or storage source |
|
|
186
186
|
| `ownerId` | `string` | — | Owner user ID (for plugins/custom code) |
|
|
187
187
|
| `auth` | `boolean | AuthCollectionConfig` | — | Mark collection as authentication collection (user management, reset password, etc.) |
|
|
@@ -367,6 +367,23 @@ avatar: {
|
|
|
367
367
|
| `postProcess` | `(pathOrUrl) => Promise<string>` | — | Transform saved path/URL after upload |
|
|
368
368
|
| `includeBucketUrl` | `boolean` | `false` | Include bucket URL in saved path |
|
|
369
369
|
| `storeUrl` | `boolean` | `false` | Save download URL instead of storage path |
|
|
370
|
+
| `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`. |
|
|
371
|
+
|
|
372
|
+
#### Per-Property Backend Binding
|
|
373
|
+
|
|
374
|
+
When using multiple storage backends, use `storageSource` to route a specific property's uploads to a named backend:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// Route this property's uploads to Firebase Storage
|
|
378
|
+
image: {
|
|
379
|
+
type: "string",
|
|
380
|
+
storage: {
|
|
381
|
+
storageSource: "firebase",
|
|
382
|
+
storagePath: "products/{entityId}",
|
|
383
|
+
acceptedFiles: ["image/*"],
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
370
387
|
|
|
371
388
|
### ImageResize Options
|
|
372
389
|
|
|
@@ -693,10 +710,10 @@ Relations are defined **directly on the property** using `type: "relation"`. The
|
|
|
693
710
|
### Many-to-One (Owning)
|
|
694
711
|
|
|
695
712
|
```typescript
|
|
696
|
-
import {
|
|
713
|
+
import { PostgresCollectionConfig } from "@rebasepro/types";
|
|
697
714
|
import authorsCollection from "./authors";
|
|
698
715
|
|
|
699
|
-
const postsCollection:
|
|
716
|
+
const postsCollection: PostgresCollectionConfig = {
|
|
700
717
|
name: "Posts",
|
|
701
718
|
slug: "posts",
|
|
702
719
|
table: "posts",
|
|
@@ -745,7 +762,7 @@ comments: {
|
|
|
745
762
|
|
|
746
763
|
| Option | Type | Default | Description |
|
|
747
764
|
|--------|------|---------|-------------|
|
|
748
|
-
| `target` | `string \| (() =>
|
|
765
|
+
| `target` | `string \| (() => CollectionConfig \| string)` | — | Target collection (use a function for lazy resolution to avoid circular imports) |
|
|
749
766
|
| `cardinality` | `"one" \| "many"` | `"one"` | Whether this references one or many records |
|
|
750
767
|
| `direction` | `"owning" \| "inverse"` | `"owning"` | Which side owns the FK or junction table |
|
|
751
768
|
| `localKey` | `string` | auto-inferred | Column on THIS table storing the FK (e.g. `"author_id"`) |
|
|
@@ -756,7 +773,7 @@ comments: {
|
|
|
756
773
|
| `inverseRelationName` | `string` | — | Name of the corresponding relation on the target collection |
|
|
757
774
|
| `onDelete` | `OnAction` | — | Cascade rule on delete |
|
|
758
775
|
| `onUpdate` | `OnAction` | — | Cascade rule on update |
|
|
759
|
-
| `overrides` | `Partial<
|
|
776
|
+
| `overrides` | `Partial<CollectionConfig>` | — | Override target collection config when rendered as subcollection tab |
|
|
760
777
|
| `fixedFilter` | `FilterValues` | — | Filter applied when selecting related entities |
|
|
761
778
|
| `includeId` | `boolean` | `true` | Show entity ID in the reference preview |
|
|
762
779
|
| `includeEntityLink` | `boolean` | `true` | Show link to open the related entity |
|
|
@@ -847,13 +864,13 @@ customer: {
|
|
|
847
864
|
|
|
848
865
|
> **See full documentation:** [Relations](https://rebase.pro/docs/collections/relations)
|
|
849
866
|
|
|
850
|
-
##
|
|
867
|
+
## Collection Callbacks (Lifecycle Hooks)
|
|
851
868
|
|
|
852
869
|
> **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.
|
|
853
870
|
|
|
854
871
|
### Generic Type Parameters
|
|
855
872
|
|
|
856
|
-
`
|
|
873
|
+
`CollectionCallbacks<M, USER>` accepts two generic type parameters:
|
|
857
874
|
|
|
858
875
|
| Parameter | Default | Description |
|
|
859
876
|
|-----------|---------|-------------|
|
|
@@ -861,7 +878,7 @@ customer: {
|
|
|
861
878
|
| `USER` | `User` | User type — extends the base `User` type with custom fields |
|
|
862
879
|
|
|
863
880
|
```typescript
|
|
864
|
-
import {
|
|
881
|
+
import { PostgresCollectionConfig, CollectionCallbacks } from "@rebasepro/types";
|
|
865
882
|
|
|
866
883
|
interface Product {
|
|
867
884
|
name: string;
|
|
@@ -870,7 +887,7 @@ interface Product {
|
|
|
870
887
|
status: string;
|
|
871
888
|
}
|
|
872
889
|
|
|
873
|
-
const callbacks:
|
|
890
|
+
const callbacks: CollectionCallbacks<Product> = {
|
|
874
891
|
beforeSave: async ({ values, status }) => {
|
|
875
892
|
// `values` is typed as Partial<Product>
|
|
876
893
|
if (values.name) {
|
|
@@ -892,12 +909,36 @@ All callbacks receive a `context` property of type `RebaseCallContext<USER>`. Th
|
|
|
892
909
|
| `context.storageSource` | `StorageSource` | File storage operations |
|
|
893
910
|
| `context.user` | `USER \| undefined` | Authenticated user (set by backend in server-side callbacks) |
|
|
894
911
|
|
|
912
|
+
### Reserved `context.user` Values
|
|
913
|
+
|
|
914
|
+
The `context.user` object is populated by the auth middleware. In server-side callbacks, it contains one of these reserved identities:
|
|
915
|
+
|
|
916
|
+
| Caller | `context.user.uid` | `context.user.roles` |
|
|
917
|
+
|---|---|---|
|
|
918
|
+
| JWT-authenticated end-user | Real user ID (e.g. `"abc123"`) | Their assigned roles (e.g. `["viewer"]`) |
|
|
919
|
+
| Server-side `rebase.data` (cron jobs, custom functions) | `"service"` | `["admin"]` |
|
|
920
|
+
| API key (default) | `"api-key:{id}"` | `["service"]` |
|
|
921
|
+
| API key (admin) | `"api-key:{id}"` | `["admin", "service"]` |
|
|
922
|
+
| Anonymous (no auth, `requireAuth: false`) | `"anon"` | `["anon"]` |
|
|
923
|
+
| Anonymous REST (no token) | `undefined` | N/A — `context.user` is not set; only the DataDriver is scoped |
|
|
924
|
+
|
|
925
|
+
> **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:
|
|
926
|
+
>
|
|
927
|
+
> ```typescript
|
|
928
|
+
> afterRead: async ({ row, context }) => {
|
|
929
|
+
> // Server-side reads (cron jobs, admin) see real values
|
|
930
|
+
> if (context.user?.roles?.includes("admin")) return row;
|
|
931
|
+
> // End-user reads get masked values
|
|
932
|
+
> return { ...row, email: "***@***.***" };
|
|
933
|
+
> }
|
|
934
|
+
> ```
|
|
935
|
+
|
|
895
936
|
> **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`.
|
|
896
937
|
|
|
897
938
|
### Callback Example
|
|
898
939
|
|
|
899
940
|
```typescript
|
|
900
|
-
const jobSubmissionsCollection:
|
|
941
|
+
const jobSubmissionsCollection: PostgresCollectionConfig = {
|
|
901
942
|
name: "Job Submissions",
|
|
902
943
|
slug: "job_submissions",
|
|
903
944
|
table: "job_submissions",
|
|
@@ -914,38 +955,35 @@ const jobSubmissionsCollection: PostgresCollection = {
|
|
|
914
955
|
},
|
|
915
956
|
|
|
916
957
|
// Runs AFTER saving — trigger side effects, sync other collections
|
|
917
|
-
afterSave: async ({ values,
|
|
958
|
+
afterSave: async ({ values, id, previousValues, context }) => {
|
|
918
959
|
if (values.status === "approved" && previousValues?.status !== "approved") {
|
|
919
960
|
await context.data.jobs.create({
|
|
920
961
|
title: values.title,
|
|
921
962
|
description: values.description,
|
|
922
963
|
company_id: values.company_id,
|
|
923
964
|
status: "published",
|
|
924
|
-
source_submission_id:
|
|
965
|
+
source_submission_id: id,
|
|
925
966
|
});
|
|
926
967
|
}
|
|
927
968
|
},
|
|
928
969
|
|
|
929
970
|
// Runs BEFORE deleting — block or validate
|
|
930
|
-
beforeDelete: async ({
|
|
931
|
-
if (
|
|
971
|
+
beforeDelete: async ({ row }) => {
|
|
972
|
+
if (row.status === "published") {
|
|
932
973
|
throw new Error("Cannot delete published submissions");
|
|
933
974
|
}
|
|
934
975
|
},
|
|
935
976
|
|
|
936
977
|
// Runs AFTER deleting — cleanup related data
|
|
937
|
-
afterDelete: async ({
|
|
938
|
-
console.log(`Submission ${
|
|
978
|
+
afterDelete: async ({ id, context }) => {
|
|
979
|
+
console.log(`Submission ${id} deleted`);
|
|
939
980
|
},
|
|
940
981
|
|
|
941
982
|
// Runs AFTER reading — transform for display
|
|
942
|
-
afterRead: async ({
|
|
983
|
+
afterRead: async ({ row }) => {
|
|
943
984
|
return {
|
|
944
|
-
...
|
|
945
|
-
|
|
946
|
-
...entity.values,
|
|
947
|
-
displayName: `${entity.values.title} (${entity.values.company_name})`
|
|
948
|
-
}
|
|
985
|
+
...row,
|
|
986
|
+
displayName: `${row.title} (${row.company_name})`
|
|
949
987
|
};
|
|
950
988
|
}
|
|
951
989
|
},
|
|
@@ -960,7 +998,7 @@ const jobSubmissionsCollection: PostgresCollection = {
|
|
|
960
998
|
| `beforeSave` | Before write to DB (after validation) | Modified `values` (`Partial<EntityValues<M>>`) | Yes (throw to block) |
|
|
961
999
|
| `afterSave` | After successful write | `void` | No |
|
|
962
1000
|
| `afterSaveError` | After a failed write | `void` | No |
|
|
963
|
-
| `afterRead` | After reading from DB | Modified
|
|
1001
|
+
| `afterRead` | After reading from DB | Modified row (`Record<string, unknown>`) | No |
|
|
964
1002
|
| `beforeDelete` | Before deletion | `void \| boolean` | Yes (throw to block) |
|
|
965
1003
|
| `afterDelete` | After successful deletion | `void` | No |
|
|
966
1004
|
|
|
@@ -971,10 +1009,10 @@ const jobSubmissionsCollection: PostgresCollection = {
|
|
|
971
1009
|
| Prop | Type | Description |
|
|
972
1010
|
|------|------|-------------|
|
|
973
1011
|
| `values` | `Partial<EntityValues<M>>` | Entity values being saved |
|
|
974
|
-
| `
|
|
1012
|
+
| `id` | `string \| number` (optional in `beforeSave`) | Entity ID (`undefined` for new entities in `beforeSave`) |
|
|
975
1013
|
| `previousValues` | `Partial<EntityValues<M>> \| undefined` | Previous values (for updates) |
|
|
976
1014
|
| `status` | `EntityStatus` | `"new"`, `"existing"`, or `"copy"` |
|
|
977
|
-
| `collection` | `
|
|
1015
|
+
| `collection` | `CollectionConfig<M>` | The collection definition |
|
|
978
1016
|
| `path` | `string` | Collection path |
|
|
979
1017
|
| `context` | `RebaseCallContext<USER>` | Context with `client`, `data`, `storageSource`, `user` |
|
|
980
1018
|
|
|
@@ -982,8 +1020,8 @@ const jobSubmissionsCollection: PostgresCollection = {
|
|
|
982
1020
|
|
|
983
1021
|
| Prop | Type | Description |
|
|
984
1022
|
|------|------|-------------|
|
|
985
|
-
| `
|
|
986
|
-
| `collection` | `
|
|
1023
|
+
| `row` | `Record<string, unknown>` | The fetched row (flat — `{ id, ...columns }`) |
|
|
1024
|
+
| `collection` | `CollectionConfig<M>` | The collection definition |
|
|
987
1025
|
| `path` | `string` | Collection path |
|
|
988
1026
|
| `context` | `RebaseCallContext<USER>` | Context |
|
|
989
1027
|
|
|
@@ -991,9 +1029,9 @@ const jobSubmissionsCollection: PostgresCollection = {
|
|
|
991
1029
|
|
|
992
1030
|
| Prop | Type | Description |
|
|
993
1031
|
|------|------|-------------|
|
|
994
|
-
| `
|
|
995
|
-
| `
|
|
996
|
-
| `collection` | `
|
|
1032
|
+
| `row` | `Record<string, unknown>` | The row being deleted (flat — `{ id, ...columns }`) |
|
|
1033
|
+
| `id` | `string \| number` | Entity ID |
|
|
1034
|
+
| `collection` | `CollectionConfig<M>` | The collection definition |
|
|
997
1035
|
| `path` | `string` | Collection path |
|
|
998
1036
|
| `context` | `RebaseCallContext<USER>` | Context |
|
|
999
1037
|
|
|
@@ -1027,7 +1065,7 @@ title: {
|
|
|
1027
1065
|
- **Cascade operations** — Use `afterDelete` to clean up related records in other collections
|
|
1028
1066
|
- **Data enrichment** — Use `afterRead` to add computed/virtual fields for display
|
|
1029
1067
|
|
|
1030
|
-
> **See full documentation:** [
|
|
1068
|
+
> **See full documentation:** [Collection Callbacks](https://rebase.pro/docs/collections/callbacks)
|
|
1031
1069
|
|
|
1032
1070
|
## Entity Actions (Custom UI Buttons)
|
|
1033
1071
|
|
|
@@ -1036,7 +1074,7 @@ title: {
|
|
|
1036
1074
|
Add an `entityActions` array to any collection definition:
|
|
1037
1075
|
|
|
1038
1076
|
```typescript
|
|
1039
|
-
const jobSubmissionsCollection:
|
|
1077
|
+
const jobSubmissionsCollection: PostgresCollectionConfig = {
|
|
1040
1078
|
name: "Job Submissions",
|
|
1041
1079
|
slug: "job_submissions",
|
|
1042
1080
|
table: "job_submissions",
|
|
@@ -1093,13 +1131,13 @@ The `onClick` and `isEnabled` handlers receive:
|
|
|
1093
1131
|
| `entity` | `Entity<M> \| undefined` | The current entity |
|
|
1094
1132
|
| `context` | `RebaseContext<USER>` | Full context (includes `snackbarController`, `authController`, etc.) |
|
|
1095
1133
|
| `path` | `string \| undefined` | Collection path |
|
|
1096
|
-
| `collection` | `
|
|
1134
|
+
| `collection` | `CollectionConfig<M> \| undefined` | Collection definition |
|
|
1097
1135
|
| `formContext` | `FormContext \| undefined` | Form state (when called from a form) |
|
|
1098
|
-
| `sideEntityController` | `
|
|
1136
|
+
| `sideEntityController` | `SidePanelController \| undefined` | Side panel control |
|
|
1099
1137
|
| `selectionController` | `SelectionController \| undefined` | Multi-select state (collection view) |
|
|
1100
1138
|
| `view` | `"collection" \| "form"` | Where the action was triggered |
|
|
1101
1139
|
| `openEntityMode` | `"side_panel" \| "full_screen" \| "split" \| "dialog"` | How the entity form is opened |
|
|
1102
|
-
| `highlightEntity` | `(entity) => void` | Highlight
|
|
1140
|
+
| `highlightEntity` | `(entity) => void` | Highlight a entity row |
|
|
1103
1141
|
| `unhighlightEntity` | `(entity) => void` | Remove highlight |
|
|
1104
1142
|
| `navigateBack` | `() => void` | Navigate back (e.g., after deleting) |
|
|
1105
1143
|
| `onCollectionChange` | `() => void` | Refresh the collection view |
|
|
@@ -1126,7 +1164,7 @@ const entityViews = [
|
|
|
1126
1164
|
<RebaseCMS collections={collections} entityViews={entityViews}/>
|
|
1127
1165
|
|
|
1128
1166
|
// Per-collection reference in collection definition
|
|
1129
|
-
const postsCollection:
|
|
1167
|
+
const postsCollection: PostgresCollectionConfig = {
|
|
1130
1168
|
name: "Posts",
|
|
1131
1169
|
slug: "posts",
|
|
1132
1170
|
table: "posts",
|
|
@@ -1175,9 +1213,9 @@ You can override built-in UI components for a specific collection by adding a `c
|
|
|
1175
1213
|
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.
|
|
1176
1214
|
|
|
1177
1215
|
```typescript
|
|
1178
|
-
import {
|
|
1216
|
+
import { PostgresCollectionConfig } from "@rebasepro/types";
|
|
1179
1217
|
|
|
1180
|
-
const productsCollection:
|
|
1218
|
+
const productsCollection: PostgresCollectionConfig = {
|
|
1181
1219
|
name: "Products",
|
|
1182
1220
|
slug: "products",
|
|
1183
1221
|
table: "products",
|
|
@@ -1223,9 +1261,9 @@ You can mark a collection as an authentication collection by setting the `auth`
|
|
|
1223
1261
|
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.
|
|
1224
1262
|
|
|
1225
1263
|
```typescript
|
|
1226
|
-
import {
|
|
1264
|
+
import { PostgresCollectionConfig } from "@rebasepro/types";
|
|
1227
1265
|
|
|
1228
|
-
const customUsersCollection:
|
|
1266
|
+
const customUsersCollection: PostgresCollectionConfig = {
|
|
1229
1267
|
name: "Members",
|
|
1230
1268
|
slug: "members",
|
|
1231
1269
|
table: "members",
|
|
@@ -1289,7 +1327,7 @@ location: {
|
|
|
1289
1327
|
Collections support **Row Level Security** via the `securityRules` array. This generates PostgreSQL RLS policies:
|
|
1290
1328
|
|
|
1291
1329
|
```typescript
|
|
1292
|
-
const postsCollection:
|
|
1330
|
+
const postsCollection: PostgresCollectionConfig = {
|
|
1293
1331
|
name: "Posts",
|
|
1294
1332
|
slug: "posts",
|
|
1295
1333
|
table: "posts",
|
|
@@ -230,6 +230,11 @@ interface CronJobContext {
|
|
|
230
230
|
| `log` | `(...args: unknown[]) => void` | Logger whose output is captured in `CronJobLogEntry.logs`. Use like `console.log`. |
|
|
231
231
|
| `client` | `RebaseClient` | Admin-level client for CRUD, auth, storage, and any SDK operation. |
|
|
232
232
|
|
|
233
|
+
> **IMPORTANT FOR AGENTS:** `ctx.client` authenticates as the **service identity** (`userId: "service"`, `roles: ["admin"]`). All data operations through `ctx.client` go through the full REST middleware pipeline (including DataHooks and Collection Callbacks). This means:
|
|
234
|
+
> - Collection Callbacks will see `context.user.uid === "service"` and `context.user.roles` containing `"admin"`
|
|
235
|
+
> - If your callbacks implement PII masking or role-based filtering, they should check for admin/service roles and skip masking for server-internal reads
|
|
236
|
+
> - The service identity bypasses RLS policies
|
|
237
|
+
|
|
233
238
|
### Using `ctx.client`
|
|
234
239
|
|
|
235
240
|
`ctx.client` is the same `RebaseClient` returned by `createRebaseClient()`. It supports all SDK operations:
|
|
@@ -118,6 +118,27 @@ app.get("/", async (c) => {
|
|
|
118
118
|
export default app;
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
### Reserved Identity Values in `c.get("user")`
|
|
122
|
+
|
|
123
|
+
The `user` object set by the auth middleware uses reserved values for system identities:
|
|
124
|
+
|
|
125
|
+
| Auth Method | `user.userId` | `user.roles` |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| JWT (end-user) | Real user ID | User's assigned roles |
|
|
128
|
+
| Service Key | `"service"` | `["admin"]` |
|
|
129
|
+
| API Key (default) | `"api-key:{id}"` | `["service"]` |
|
|
130
|
+
| API Key (admin) | `"api-key:{id}"` | `["admin", "service"]` |
|
|
131
|
+
| Anonymous | `"anon"` | `["anon"]` |
|
|
132
|
+
|
|
133
|
+
> **TIP:** Use these to differentiate internal vs. external callers in your custom functions:
|
|
134
|
+
> ```typescript
|
|
135
|
+
> app.get("/sensitive-data", async (c) => {
|
|
136
|
+
> const user = c.get("user");
|
|
137
|
+
> const isInternal = user?.userId === "service" || user?.roles?.includes("admin");
|
|
138
|
+
> // Return full or masked data based on identity
|
|
139
|
+
> });
|
|
140
|
+
> ```
|
|
141
|
+
|
|
121
142
|
## Invoking Functions from the Frontend
|
|
122
143
|
|
|
123
144
|
> **CRITICAL FOR AGENTS**: When calling custom backend functions from the frontend, **ALWAYS** use `client.functions.invoke()`. **NEVER** use raw `fetch()`, manually construct URLs, or manually extract auth tokens from `localStorage`. The SDK handles all of this automatically.
|
|
@@ -367,7 +367,7 @@ All variables recognized by the base `rebaseEnvSchema`:
|
|
|
367
367
|
| `DB_POOL_CONNECT_TIMEOUT` | `string` → `number` | No | `"10000"` → `10000` | Connection timeout (ms) |
|
|
368
368
|
| `DATABASE_DIRECT_URL` | `string` (URL) | No | — | Direct database URL (bypasses pooler) |
|
|
369
369
|
| `DATABASE_READ_URL` | `string` (URL) | No | — | Read replica database URL |
|
|
370
|
-
| `STORAGE_TYPE` | `enum` | No | `"local"` | `"local"` or `"
|
|
370
|
+
| `STORAGE_TYPE` | `enum` | No | `"local"` | `"local"`, `"s3"`, or `"gcs"` |
|
|
371
371
|
| `STORAGE_PATH` | `string` | No | — | Local file storage directory |
|
|
372
372
|
| `FORCE_LOCAL_STORAGE` | `optionalBoolString` | No | `undefined` → `false` | Suppress local-storage-in-production warning |
|
|
373
373
|
| `S3_BUCKET` | `string` | If S3 | — | S3 bucket name |
|
|
@@ -376,6 +376,9 @@ All variables recognized by the base `rebaseEnvSchema`:
|
|
|
376
376
|
| `S3_SECRET_ACCESS_KEY` | `string` | If S3 | — | S3 secret key |
|
|
377
377
|
| `S3_ENDPOINT` | `string` (URL) | No | — | Custom S3 endpoint (MinIO, R2) |
|
|
378
378
|
| `S3_FORCE_PATH_STYLE` | `optionalBoolString` | No | `undefined` → `false` | Use path-style S3 URLs (required for MinIO) |
|
|
379
|
+
| `GCS_BUCKET` | `string` | If GCS | — | Google Cloud Storage bucket name |
|
|
380
|
+
| `GCS_PROJECT_ID` | `string` | If GCS | — | GCP project ID for GCS |
|
|
381
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | `string` | If GCS (non-GCP) | — | Path to GCP service account JSON key file (not needed on GCP with default credentials) |
|
|
379
382
|
|
|
380
383
|
### Zod Type Helpers
|
|
381
384
|
|
|
@@ -573,7 +576,13 @@ Internet → Cloud Run (HTTPS, auto-scaling) → Cloud SQL PostgreSQL
|
|
|
573
576
|
--allow-unauthenticated
|
|
574
577
|
```
|
|
575
578
|
4. **Custom Domain** — Map a custom domain in Cloud Run settings. Cloud Run provides HTTPS automatically.
|
|
576
|
-
5. **File Storage** — Use
|
|
579
|
+
5. **File Storage** — Use native GCS support with `STORAGE_TYPE=gcs`:
|
|
580
|
+
```bash
|
|
581
|
+
STORAGE_TYPE=gcs
|
|
582
|
+
GCS_BUCKET=your-rebase-uploads
|
|
583
|
+
GCS_PROJECT_ID=your-gcp-project-id
|
|
584
|
+
```
|
|
585
|
+
On Cloud Run, the default service account credentials are used automatically — no key file needed. Alternatively, you can fall back to `STORAGE_TYPE=s3` with the GCS S3-compatible interop endpoint.
|
|
577
586
|
|
|
578
587
|
### Cloud SQL Auth Proxy (Connection String)
|
|
579
588
|
|
|
@@ -762,7 +771,7 @@ Schedule automated PostgreSQL backups via cron on the host:
|
|
|
762
771
|
0 3 * * * root docker compose -f /path/to/docker-compose.yml exec -T db pg_dump -U rebase rebase | gzip > /backups/rebase-$(date +\%Y\%m\%d).sql.gz
|
|
763
772
|
```
|
|
764
773
|
|
|
765
|
-
> **TIP:** Hetzner also offers managed
|
|
774
|
+
> **TIP:** Hetzner also offers managed entities (server-level) and the option to attach a Hetzner Volume for data that survives server rebuilds. For production, strongly consider both.
|
|
766
775
|
|
|
767
776
|
---
|
|
768
777
|
|
|
@@ -408,7 +408,7 @@ auth: {
|
|
|
408
408
|
|
|
409
409
|
## `rebase.email` Singleton
|
|
410
410
|
|
|
411
|
-
When email is configured, the email service is automatically attached to the `rebase` server-side singleton as `rebase.email`. This allows sending emails programmatically from custom functions, cron jobs, and
|
|
411
|
+
When email is configured, the email service is automatically attached to the `rebase` server-side singleton as `rebase.email`. This allows sending emails programmatically from custom functions, cron jobs, and collection callbacks.
|
|
412
412
|
|
|
413
413
|
```typescript
|
|
414
414
|
import { rebase } from "@rebasepro/server-core";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rebase-entity-history
|
|
3
|
-
description: Guide for tracking and reverting entity changes with Rebase's built-in history/audit-log system. Use this skill when the user needs entity versioning, change tracking, audit logs, reverting to previous versions, or understanding how history
|
|
3
|
+
description: Guide for tracking and reverting entity changes with Rebase's built-in history/audit-log system. Use this skill when the user needs entity versioning, change tracking, audit logs, reverting to previous versions, or understanding how history entities work.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Rebase Entity History
|
|
7
7
|
|
|
8
|
-
Rebase includes a built-in entity history system that records
|
|
8
|
+
Rebase includes a built-in entity history system that records entities of every create, update, and delete operation. History entries are stored in a dedicated PostgreSQL table and exposed via REST API endpoints. The Studio admin panel provides a visual history tab with one-click revert.
|
|
9
9
|
|
|
10
10
|
> **IMPORTANT FOR AGENTS:** Entity history requires **two** things to be enabled: (1) the global `history` flag in the backend config, AND (2) `history: true` on each individual collection. If either is missing, no history is recorded for that collection.
|
|
11
11
|
|
|
@@ -49,9 +49,9 @@ history: { retention: 30 },
|
|
|
49
49
|
On each collection that should be tracked, set `history: true`:
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
|
-
import {
|
|
52
|
+
import { PostgresCollectionConfig } from "@rebasepro/types";
|
|
53
53
|
|
|
54
|
-
const productsCollection:
|
|
54
|
+
const productsCollection: PostgresCollectionConfig = {
|
|
55
55
|
slug: "products",
|
|
56
56
|
name: "Products",
|
|
57
57
|
table: "products",
|
|
@@ -100,7 +100,7 @@ Each history entry has the following structure:
|
|
|
100
100
|
| `entity_id` | `string` | The ID of the entity this entry belongs to |
|
|
101
101
|
| `action` | `"create" \| "update" \| "delete"` | What operation was performed |
|
|
102
102
|
| `changed_fields` | `string[] \| null` | Array of field names that changed (only for updates) |
|
|
103
|
-
| `values` | `Record<string, unknown> \| null` | Full entity values
|
|
103
|
+
| `values` | `Record<string, unknown> \| null` | Full entity values entity at the time of the change |
|
|
104
104
|
| `previous_values` | `Record<string, unknown> \| null` | Previous entity values (only for updates) |
|
|
105
105
|
| `updated_by` | `string \| null` | UID of the user who made the change |
|
|
106
106
|
| `updated_at` | `string` (ISO 8601) | Timestamp of when the change was recorded |
|
|
@@ -292,7 +292,7 @@ CREATE TABLE IF NOT EXISTS rebase.entity_history (
|
|
|
292
292
|
entity_id TEXT NOT NULL,
|
|
293
293
|
action TEXT NOT NULL, -- 'create', 'update', or 'delete'
|
|
294
294
|
changed_fields TEXT[], -- array of changed field names (updates only)
|
|
295
|
-
"values" JSONB, -- full entity values
|
|
295
|
+
"values" JSONB, -- full entity values entity
|
|
296
296
|
previous_values JSONB, -- previous values (updates only)
|
|
297
297
|
updated_by TEXT, -- user UID who made the change
|
|
298
298
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
@@ -310,7 +310,7 @@ CREATE INDEX IF NOT EXISTS idx_history_time
|
|
|
310
310
|
|
|
311
311
|
### Storage Characteristics
|
|
312
312
|
|
|
313
|
-
- **Values are stored as JSONB** — full
|
|
313
|
+
- **Values are stored as JSONB** — full entities, not diffs. This makes revert simple and reliable but uses more storage.
|
|
314
314
|
- **All collections share one table** — the `table_name` column distinguishes between collections.
|
|
315
315
|
- **Entity IDs are stored as text** — regardless of the original column type.
|
|
316
316
|
- **Indexes** cover `(table_name, entity_id)` and `(table_name, entity_id, updated_at DESC)` for fast per-entity lookups.
|
|
@@ -382,7 +382,7 @@ No additional configuration is needed — the History tab appears automatically
|
|
|
382
382
|
|
|
383
383
|
There is currently **no dedicated client SDK method** for entity history. History is accessed via the REST API directly (as shown in the endpoints section above).
|
|
384
384
|
|
|
385
|
-
The Studio admin panel uses an internal `
|
|
385
|
+
The Studio admin panel uses an internal `useHistory` React hook that:
|
|
386
386
|
- Fetches history from `GET /api/data/:slug/:entityId/history`
|
|
387
387
|
- Supports pagination via offset-based loading
|
|
388
388
|
- Provides a `revert()` function that calls `POST /api/data/:slug/:entityId/history/:historyId/revert`
|
|
@@ -462,7 +462,7 @@ console.log(`Server running at http://localhost:${env.PORT}`);
|
|
|
462
462
|
|
|
463
463
|
### Storage Growth
|
|
464
464
|
|
|
465
|
-
- Each history entry stores **full JSONB
|
|
465
|
+
- Each history entry stores **full JSONB entities** of both current and previous values. For entities with large text fields or complex nested structures, this can grow quickly.
|
|
466
466
|
- Default retention limits: **200 entries per entity** and **90 days TTL**. Adjust these based on your use case.
|
|
467
467
|
- All collections share the single `rebase.entity_history` table — monitor its size with:
|
|
468
468
|
|
|
@@ -341,11 +341,11 @@ ws.onmessage = (event) => {
|
|
|
341
341
|
| `broadcast` | `{ channel, event, payload }` | Send a message to channel members |
|
|
342
342
|
| `presence_track` | `{ channel, state }` | Track presence with custom state (auto-joins channel) |
|
|
343
343
|
| `presence_untrack` | `{ channel }` | Stop tracking presence |
|
|
344
|
-
| `presence_state` | `{ channel }` | Request full presence
|
|
344
|
+
| `presence_state` | `{ channel }` | Request full presence entity |
|
|
345
345
|
| `FETCH_COLLECTION` | `FetchCollectionProps` | One-shot collection fetch (request/response) |
|
|
346
346
|
| `FETCH_ENTITY` | `FetchEntityProps` | One-shot entity fetch |
|
|
347
|
-
| `SAVE_ENTITY` | `SaveEntityProps` | Create or update
|
|
348
|
-
| `DELETE_ENTITY` | `DeleteEntityProps` | Delete
|
|
347
|
+
| `SAVE_ENTITY` | `SaveEntityProps` | Create or update a entity |
|
|
348
|
+
| `DELETE_ENTITY` | `DeleteEntityProps` | Delete a entity |
|
|
349
349
|
| `COUNT_ENTITIES` | `FetchCollectionProps` | Count entities matching criteria |
|
|
350
350
|
| `CHECK_UNIQUE_FIELD` | `{ path, name, value, entityId?, collection? }` | Check field uniqueness |
|
|
351
351
|
|
|
@@ -359,7 +359,7 @@ ws.onmessage = (event) => {
|
|
|
359
359
|
| `entity_update` | `{ subscriptionId, entity }` | Single entity data (entity or `null` if deleted) |
|
|
360
360
|
| `collection_entity_patch` | `{ subscriptionId, entityId, entity }` | Instant single-entity patch for a collection subscription |
|
|
361
361
|
| `broadcast` | `{ channel, event, payload }` | Broadcast from another channel member |
|
|
362
|
-
| `presence_state` | `{ channel, presences }` | Full presence
|
|
362
|
+
| `presence_state` | `{ channel, presences }` | Full presence entity |
|
|
363
363
|
| `presence_diff` | `{ channel, joins, leaves }` | Incremental presence update |
|
|
364
364
|
| `ERROR` | `{ requestId?, payload: { error: { message, code } } }` | General error |
|
|
365
365
|
| `FETCH_COLLECTION_SUCCESS` | `{ requestId, payload: { entities } }` | Response to FETCH_COLLECTION |
|
|
@@ -687,7 +687,7 @@ If 10 entities change within 300ms, only **one** database query is executed for
|
|
|
687
687
|
|
|
688
688
|
## Nested Relation Updates
|
|
689
689
|
|
|
690
|
-
The realtime engine handles nested relation paths (e.g., `"posts/70/tags"`). When
|
|
690
|
+
The realtime engine handles nested relation paths (e.g., `"posts/70/tags"`). When a entity changes at a nested path:
|
|
691
691
|
|
|
692
692
|
1. The exact path is notified (`"posts/70/tags"`).
|
|
693
693
|
2. All parent paths are also notified (`"posts"`, `"posts/70"`).
|
|
@@ -437,6 +437,29 @@ const { items, nextPageToken } = await rebase.storage.listObjects('prefix/', {
|
|
|
437
437
|
});
|
|
438
438
|
```
|
|
439
439
|
|
|
440
|
+
### Multi-Backend Storage
|
|
441
|
+
|
|
442
|
+
`rebase.storage` accesses the **default** storage source. For multi-backend setups (e.g. S3 + GCS/Firebase), collection properties can specify `storage.storageSource` in their schema to route uploads to a named backend.
|
|
443
|
+
|
|
444
|
+
On the frontend, register direct storage sources via the `storageSources` prop on `<Rebase>`:
|
|
445
|
+
|
|
446
|
+
```tsx
|
|
447
|
+
import { getStorage } from "firebase/storage";
|
|
448
|
+
|
|
449
|
+
const firebaseStorageSource = getStorage(firebaseApp);
|
|
450
|
+
|
|
451
|
+
<Rebase
|
|
452
|
+
client={rebaseClient}
|
|
453
|
+
storageSources={[
|
|
454
|
+
{ key: "firebase", engine: "firebase", transport: "direct", source: firebaseStorageSource }
|
|
455
|
+
]}
|
|
456
|
+
>
|
|
457
|
+
{children}
|
|
458
|
+
</Rebase>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The system resolves the correct source per-property via `StorageSourcesContext` (mirroring `DataSourcesContext`). Properties bound to a `direct` storage source bypass the Rebase backend and upload/download directly to the provider (e.g. Firebase Storage), while `server` sources route through the Rebase API with a `?storageId` query parameter for backend routing.
|
|
462
|
+
|
|
440
463
|
## Custom Functions
|
|
441
464
|
|
|
442
465
|
Invoke custom server-side functions defined in the backend:
|