@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.
@@ -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 `PostgresCollection` interface from `@rebasepro/types`. Each collection maps to a database table (via the `table` property) and generates:
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 | **PostgresCollection** | FirebaseCollection or legacy |
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 { PostgresCollection } from "@rebasepro/types";
61
+ import { PostgresCollectionConfig } from "@rebasepro/types";
62
62
 
63
- const productsCollection: PostgresCollection = {
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` | `EntityCallbacks<M, USER>` | — | Lifecycle hooks (see Entity Callbacks section) |
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` | `() => EntityCollection[]` | — | Nested child collections (populated automatically) |
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 { PostgresCollection } from "@rebasepro/types";
713
+ import { PostgresCollectionConfig } from "@rebasepro/types";
697
714
  import authorsCollection from "./authors";
698
715
 
699
- const postsCollection: PostgresCollection = {
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 \| (() => EntityCollection \| string)` | — | Target collection (use a function for lazy resolution to avoid circular imports) |
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<EntityCollection>` | — | Override target collection config when rendered as subcollection tab |
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
- ## Entity Callbacks (Lifecycle Hooks)
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
- `EntityCallbacks<M, USER>` accepts two generic type parameters:
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 { PostgresCollection, EntityCallbacks } from "@rebasepro/types";
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: EntityCallbacks<Product> = {
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: PostgresCollection = {
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, entityId, previousValues, context }) => {
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: entityId,
965
+ source_submission_id: id,
925
966
  });
926
967
  }
927
968
  },
928
969
 
929
970
  // Runs BEFORE deleting — block or validate
930
- beforeDelete: async ({ entity }) => {
931
- if (entity.values.status === "published") {
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 ({ entityId, context }) => {
938
- console.log(`Submission ${entityId} deleted`);
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 ({ entity }) => {
983
+ afterRead: async ({ row }) => {
943
984
  return {
944
- ...entity,
945
- values: {
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 `entity` (`Entity<M>`) | No |
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
- | `entityId` | `string \| number` (optional in `beforeSave`) | Entity ID (`undefined` for new entities in `beforeSave`) |
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` | `EntityCollection<M>` | The collection definition |
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
- | `entity` | `Entity<M>` | The fetched entity |
986
- | `collection` | `EntityCollection<M>` | The collection definition |
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
- | `entity` | `Entity<M>` | The entity being deleted |
995
- | `entityId` | `string \| number` | Entity ID |
996
- | `collection` | `EntityCollection<M>` | The collection definition |
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:** [Entity Callbacks](https://rebase.pro/docs/collections/callbacks)
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: PostgresCollection = {
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` | `EntityCollection<M> \| undefined` | Collection definition |
1134
+ | `collection` | `CollectionConfig<M> \| undefined` | Collection definition |
1097
1135
  | `formContext` | `FormContext \| undefined` | Form state (when called from a form) |
1098
- | `sideEntityController` | `SideEntityController \| undefined` | Side panel control |
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 an entity row |
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: PostgresCollection = {
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 { PostgresCollection } from "@rebasepro/types";
1216
+ import { PostgresCollectionConfig } from "@rebasepro/types";
1179
1217
 
1180
- const productsCollection: PostgresCollection = {
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 { PostgresCollection } from "@rebasepro/types";
1264
+ import { PostgresCollectionConfig } from "@rebasepro/types";
1227
1265
 
1228
- const customUsersCollection: PostgresCollection = {
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: PostgresCollection = {
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 `"s3"` |
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 `STORAGE_TYPE=s3` with a GCS bucket via the S3-compatible interop endpoint, or use a dedicated S3 provider like Cloudflare R2.
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 snapshots (server-level) and the option to attach a Hetzner Volume for data that survives server rebuilds. For production, strongly consider both.
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 entity callbacks.
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 snapshots work.
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 snapshots 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.
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 { PostgresCollection } from "@rebasepro/types";
52
+ import { PostgresCollectionConfig } from "@rebasepro/types";
53
53
 
54
- const productsCollection: PostgresCollection = {
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 snapshot at the time of the change |
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 snapshot
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 snapshots, not diffs. This makes revert simple and reliable but uses more storage.
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 `useEntityHistory` React hook that:
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 snapshots** of both current and previous values. For entities with large text fields or complex nested structures, this can grow quickly.
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 snapshot |
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 an entity |
348
- | `DELETE_ENTITY` | `DeleteEntityProps` | Delete an entity |
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 snapshot |
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 an entity changes at a nested path:
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: