@rebasepro/agent-skills 0.7.0 → 0.8.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rebase
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@rebasepro/agent-skills",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "Bundled AI agent skills for Rebase",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/rebasepro/rebase.git",
10
+ "directory": "rebase-agent-skills"
11
+ },
7
12
  "files": [
8
13
  "skills/"
9
14
  ],
@@ -11,5 +16,6 @@
11
16
  ".": "./package.json",
12
17
  "./package.json": "./package.json",
13
18
  "./skills": "./skills/"
14
- }
15
- }
19
+ },
20
+ "scripts": {}
21
+ }
@@ -5,7 +5,7 @@ description: Guide for setting up and using Rebase Authentication, roles, Row-Le
5
5
 
6
6
  # Rebase Authentication
7
7
 
8
- Rebase ships a complete, built-in authentication system with JWT sessions, OAuth, MFA/TOTP, API keys, Row-Level Security, and lifecycle hooks — or you can plug in an external provider (Clerk, Auth0, Firebase Auth) via the `AuthAdapter` interface.
8
+ Rebase ships a complete, built-in authentication system with JWT sessions, OAuth, MFA/TOTP, API keys, Row-Level Security, and lifecycle hooks — or you can plug in an external auth system (e.g., Clerk, Auth0, or custom identity providers) via the `AuthAdapter` interface.
9
9
 
10
10
  > **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` skill first. The auth system is configured inside `initializeRebaseBackend()` which is covered there.
11
11
 
@@ -33,7 +33,7 @@ Rebase ships a complete, built-in authentication system with JWT sessions, OAuth
33
33
 
34
34
  Authentication is configured via the `auth` property of `initializeRebaseBackend()`. It accepts **either** a `RebaseAuthConfig` object (built-in auth) or an `AuthAdapter` (external auth).
35
35
 
36
- > **Auth & multiple data sources.** The built-in auth system (users, sessions, API keys) is bootstrapped on the **default** data source — the auth collection must live there (the backend warns at boot otherwise). **RLS only protects Postgres**: server collections on engines without row-level security (e.g. MongoDB) still require authentication but enforce authorization at the app layer (the backend warns for these). **Direct data sources (e.g. Firestore) bypass Rebase auth entirely** — they're governed by the external backend's own rules/token; use an `AuthAdapter` (e.g. Firebase Auth) to unify identity. See the **rebase-collections** skill for the data-source model.
36
+ > **Auth & multiple data sources.** The built-in auth system (users, sessions, API keys) is bootstrapped on the **default** data source — the auth collection must live there (the backend warns at boot otherwise). **RLS only protects Postgres**: server collections on engines without row-level security (e.g. MongoDB) still require authentication but enforce authorization at the app layer (the backend warns for these). **Direct data sources (e.g. Firestore) bypass Rebase auth entirely** — they're governed by the external backend's own rules/token; use an `AuthAdapter` to unify identity. See the **rebase-collections** skill for the data-source model.
37
37
 
38
38
  ### RebaseAuthConfig
39
39
 
@@ -47,7 +47,7 @@ Authentication is configured via the `auth` property of `initializeRebaseBackend
47
47
  | `allowRegistration` | `boolean` | `false` | Enable self-service registration via `POST /auth/register`. |
48
48
  | `serviceKey` | `string` | — | Static secret for server-to-server auth. Must be ≥ 32 characters. Requests with `Authorization: Bearer <serviceKey>` get admin access. |
49
49
  | `defaultRole` | `string` | — | Role ID assigned to new users (except the first user, who always gets `"admin"`). **Must NOT be `"admin"`** — throws a security error at startup. |
50
- | `providers` | `OAuthProvider<any>[]` | `[]` | Custom OAuth providers (use alongside the shorthand configs below). |
50
+ | `providers` | `OAuthProvider<unknown>[]` | `[]` | **Canonical** OAuth provider array. Use `create*Provider` factories or pass custom providers. Named shorthand fields below are merged into this array at startup. |
51
51
  | `hooks` | `AuthHooks` | — | [Lifecycle hooks](#auth-lifecycle-hooks) to customize passwords, credentials, and auth events. |
52
52
  | `email` | `EmailConfig` | — | [Email configuration](#email-configuration) for password resets, verification, and welcome emails. |
53
53
  | `google` | `{ clientId, clientSecret? }` | — | Google OAuth shorthand. |
@@ -351,14 +351,14 @@ hooks: {
351
351
  }
352
352
  ```
353
353
 
354
- ### Example: Firebase Custom Token Bridge
354
+ ### Example: External Token Bridge (e.g. custom auth system)
355
355
 
356
356
  ```typescript
357
357
  import admin from "firebase-admin";
358
358
 
359
359
  hooks: {
360
360
  transformAuthResponse: async (response, context) => {
361
- // Generate a Firebase Custom Token for the authenticated user
361
+ // Generate a custom provider token for the authenticated user
362
362
  const firebaseToken = await admin.auth().createCustomToken(context.userId);
363
363
  return {
364
364
  ...response,
@@ -371,7 +371,7 @@ hooks: {
371
371
  }
372
372
  ```
373
373
 
374
- The frontend can then call `signInWithCustomToken(firebaseToken)` immediately after login.
374
+ The frontend can then call `signInWithCustomToken(providerToken)` immediately after login.
375
375
 
376
376
  ---
377
377
 
@@ -664,7 +664,7 @@ All login/register/OAuth endpoints return:
664
664
  }
665
665
  ```
666
666
 
667
- > **TIP:** Use the `transformAuthResponse` hook to inject additional tokens (e.g., Firebase Custom Tokens) or metadata into this response. See [Auth Lifecycle Hooks](#auth-lifecycle-hooks).
667
+ > **TIP:** Use the `transformAuthResponse` hook to inject additional tokens (e.g., external system tokens) or metadata into this response. See [Auth Lifecycle Hooks](#auth-lifecycle-hooks).
668
668
 
669
669
  ### Error Response Format
670
670
 
@@ -857,6 +857,21 @@ Requests with the `serviceKey` bypass RLS — they receive admin-level access wi
857
857
 
858
858
  API keys use a service identity for RLS scoping: `uid: "api-key:{id}"`, `roles: ["service"]` (or `["admin", "service"]` when `admin: true`). They do not inherit the `created_by` user's identity.
859
859
 
860
+ ### Reserved System Identities
861
+
862
+ The auth middleware assigns these reserved identities automatically. They are visible in `context.user` (Entity Callbacks / DataHooks) and `c.get("user")` (custom functions):
863
+
864
+ | Auth Method | `userId` | `roles` | When It Occurs |
865
+ |---|---|---|---|
866
+ | JWT (end-user) | Real user ID (e.g. `"abc123"`) | User's assigned roles (e.g. `["viewer"]`) | Normal authenticated requests |
867
+ | Service Key | `"service"` | `["admin"]` | Server-side `rebase.data` calls, cron jobs, or any request with `Authorization: Bearer <serviceKey>` |
868
+ | API Key (default) | `"api-key:{id}"` | `["service"]` | Machine-to-machine API key requests |
869
+ | API Key (admin) | `"api-key:{id}"` | `["admin", "service"]` | Admin API key requests |
870
+ | Anonymous | `"anon"` | `["anon"]` | Unauthenticated when `requireAuth: false` |
871
+ | No token + `requireAuth: true` | — | — | **Rejected (401)** |
872
+
873
+ > **IMPORTANT FOR AGENTS:** Server-side `rebase.data` (the singleton used in cron jobs, custom functions, and webhooks) is built with `createRebaseClient({ token: serviceKey })`. It round-trips through the REST API, so all middleware, DataHooks, and Entity Callbacks fire with `userId: "service"`, `roles: ["admin"]`. This lets developers distinguish server-internal reads from end-user reads in callbacks.
874
+
860
875
  ---
861
876
 
862
877
  ## Rate Limiting
@@ -917,7 +932,7 @@ const myLimiter = createRateLimiter({
917
932
 
918
933
  ## Custom Auth Adapters
919
934
 
920
- For external auth systems (Clerk, Auth0, Firebase Auth, or custom JWT), use the `AuthAdapter` interface or the `createCustomAuthAdapter()` helper.
935
+ For external auth systems (Clerk, Auth0, custom providers, or custom JWT), use the `AuthAdapter` interface or the `createCustomAuthAdapter()` helper.
921
936
 
922
937
  ### AuthAdapter Interface
923
938
 
@@ -1241,6 +1256,8 @@ When a request includes `Authorization: Bearer <serviceKey>`:
1241
1256
  - Comparison is done with constant-time comparison to prevent timing attacks.
1242
1257
  - Must be ≥ 32 characters (validated at startup).
1243
1258
 
1259
+ > **TIP:** In Entity Callbacks and DataHooks, server-side `rebase.data` calls appear as `userId: "service"`, `roles: ["admin"]`. Use this to skip masking, bypass rate limits, or grant elevated access in your callback logic.
1260
+
1244
1261
  ### Token Rotation
1245
1262
 
1246
1263
  Refresh tokens are rotated on every use:
@@ -1281,3 +1298,4 @@ All auth endpoints validate input with Zod schemas:
1281
1298
  - Source: `packages/types/src/types/auth_adapter.ts` — `AuthAdapter` interface
1282
1299
  - Source: `packages/server-core/src/auth/rls-scope.ts` — RLS scoping
1283
1300
  - Source: `packages/server-core/src/email/types.ts` — Email configuration
1301
+ - **Reserved Identities**: `"service"` / `"anon"` / `"api-key:{id}"` — see [Row-Level Security > Reserved System Identities](#reserved-system-identities)
@@ -388,7 +388,7 @@ When `extend` is provided, the base `rebaseEnvSchema` is merged (`.merge()`) wit
388
388
  | `DB_POOL_MAX` | `string` → `number` | `"20"` | No | Max database pool connections |
389
389
  | `DB_POOL_IDLE_TIMEOUT` | `string` → `number` | `"30000"` | No | Pool idle timeout (ms) |
390
390
  | `DB_POOL_CONNECT_TIMEOUT` | `string` → `number` | `"10000"` | No | Pool connect timeout (ms) |
391
- | `STORAGE_TYPE` | `"local" \| "s3"` | `"local"` | No | File storage backend type |
391
+ | `STORAGE_TYPE` | `"local" \| "s3" \| "gcs"` | `"local"` | No | File storage backend type |
392
392
  | `STORAGE_PATH` | `string` | — | No | Local storage directory path |
393
393
  | `FORCE_LOCAL_STORAGE` | `"true" \| "false"` | — | No | Force local storage even in production |
394
394
  | `S3_BUCKET` | `string` | — | When S3 | S3 bucket name |
@@ -397,6 +397,9 @@ When `extend` is provided, the base `rebaseEnvSchema` is merged (`.merge()`) wit
397
397
  | `S3_SECRET_ACCESS_KEY` | `string` | — | When S3 | S3 secret key |
398
398
  | `S3_ENDPOINT` | `string` (URL) | — | No | Custom S3 endpoint (MinIO, R2, etc.) |
399
399
  | `S3_FORCE_PATH_STYLE` | `"true" \| "false"` | — | No | Use path-style S3 URLs |
400
+ | `GCS_BUCKET` | `string` | — | When GCS | GCS/Firebase Storage bucket name |
401
+ | `GCS_PROJECT_ID` | `string` | — | When GCS | GCP project ID |
402
+ | `GOOGLE_APPLICATION_CREDENTIALS` | `string` (path) | — | When GCS | Path to GCP service account JSON |
400
403
 
401
404
  ### Production Validations
402
405
 
@@ -438,7 +441,7 @@ import { initializeRebaseBackend, RebaseBackendConfig } from "@rebasepro/server-
438
441
  | `bootstrappers` | `BackendBootstrapper[]` | `[]` | Database bootstrappers. Use one per engine for multiple engines in a single instance (e.g. Postgres + MongoDB); mark one `isDefault` |
439
442
  | `dataSources` | `DataSourceDefinition[]` | `[]` | Declared data sources (`key`, `engine`, `transport`). Drives capabilities and the server-vs-direct distinction. Collections on a `direct`/`custom` transport are client-only — the backend skips data routes for them. Server engines need no entry |
440
443
  | `auth` | `RebaseAuthConfig \| AuthAdapter` | — | Authentication config or pluggable adapter |
441
- | `storage` | `BackendStorageConfig \| StorageController \| Record<string, ...>` | — | File storage configuration |
444
+ | `storage` | `BackendStorageConfig \| StorageController \| Record<string, ...>` | — | File storage configuration. Supports `"local"`, `"s3"`, and `"gcs"` (GCS/Firebase Storage) backends. Use `Record<string, StorageController>` for multi-backend setups with named sources |
442
445
  | `history` | `unknown` | — | Entity history/audit-log configuration |
443
446
  | `defaultSecurityRules` | `SecurityRule[]` | — | Default RLS rules for collections without their own |
444
447
  | `enableSwagger` | `boolean` | `true` | Enable OpenAPI spec at `/api/docs` and Swagger UI at `/api/swagger` (dev only) |
@@ -546,6 +549,8 @@ await initializeRebaseBackend({
546
549
  ? { clientId: env.GOOGLE_CLIENT_ID }
547
550
  : undefined,
548
551
  },
552
+ // Single backend: { type: "local" | "s3" | "gcs" }
553
+ // Multi-backend: Record<string, StorageController> with named sources
549
554
  storage: { type: env.STORAGE_TYPE },
550
555
  defaultSecurityRules: [
551
556
  { operation: "select", access: "public" },
@@ -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
 
@@ -892,6 +909,30 @@ 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 ({ entity, context }) => {
929
+ > // Server-side reads (cron jobs, admin) see real values
930
+ > if (context.user?.roles?.includes("admin")) return entity;
931
+ > // End-user reads get masked values
932
+ > return { ...entity, values: { ...entity.values, 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
@@ -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 Entity Callbacks). This means:
234
+ > - Entity 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
 
@@ -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:
@@ -136,6 +136,14 @@ export async function scopeDataDriver(
136
136
  | Anonymous (`requireAuth: false`) | `"anon"` | `["anon"]` | RLS with anonymous identity |
137
137
  | No token + `requireAuth: true` | — | — | **Rejected (401)** |
138
138
 
139
+ > **IMPORTANT FOR AGENTS:** These are **reserved system identity values** that the middleware injects automatically. When writing DataHooks or Entity Callbacks, developers should use these identities to gate behavior:
140
+ > - `uid: "service"` + `roles: ["admin"]` — Server-side `rebase.data` calls (cron jobs, custom functions using `rebase.data`, webhooks). These go through the full middleware pipeline authenticated with the service key.
141
+ > - `uid: "anon"` + `roles: ["anon"]` — Unauthenticated requests when `requireAuth: false`. **Note:** for anonymous REST requests, `context.user` in Entity Callbacks may be `undefined`; only the DataDriver is scoped with the anon identity. For WebSocket connections, a full `User` object with `uid: "anon"` is provided.
142
+ > - `uid: "api-key:{id}"` + `roles: ["service"]` (or `["admin", "service"]`) — API key requests.
143
+ > - Real user IDs and roles for JWT-authenticated requests.
144
+ >
145
+ > **Key insight:** `rebase.data` (the server-side singleton) is NOT a raw admin driver — it round-trips through the REST API using the service key, so all DataHooks and Entity Callbacks fire with `uid: "service"`, `roles: ["admin"]`. This means callbacks can distinguish server-internal reads from end-user reads by checking `context.user?.roles?.includes("admin")`.
146
+
139
147
  ---
140
148
 
141
149
  ## Layer 2: API Key Permission Guard
@@ -602,5 +610,6 @@ Use this checklist when setting up security for a Rebase project:
602
610
  - **REST API Generator**: `packages/server-core/src/api/rest/api-generator.ts` — DataHooks integration
603
611
  - **Backend Hooks Types**: `packages/types/src/types/backend_hooks.ts` — `DataHooks`, `BackendHooks` interfaces
604
612
  - **Backend Init**: `packages/server-core/src/init.ts` — `hooks` config property
613
+ - **Reserved Identity Values**: See Identity Types table above — `"service"`, `"anon"`, `"api-key:{id}"` are system-assigned identities in `context.user` / `ctx.requestUser`
605
614
  - **Entity Callbacks**: See `rebase-collections` skill → Entity Callbacks section
606
615
  - **Auth Configuration**: See `rebase-auth` skill → Server-Side Configuration section
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: rebase-storage
3
- description: Guide for setting up and using file storage in Rebase. Use this skill when the user needs to configure S3 or local file storage, handle file uploads, TUS resumable uploads, image transformations, or integrate the media manager.
3
+ description: Guide for setting up and using file storage in Rebase. Use this skill when the user needs to configure S3, Google Cloud Storage (GCS), or local file storage, handle file uploads, TUS resumable uploads, image transformations, multi-backend frontend storage sources, or integrate the media manager.
4
4
  ---
5
5
 
6
6
  # Rebase Storage
7
7
 
8
- Rebase provides built-in file storage with support for local filesystem and S3-compatible services, TUS v1.0.0 resumable uploads, on-the-fly image transformation, and a multi-backend registry.
8
+ Rebase provides built-in file storage with support for local filesystem, S3-compatible services, and Google Cloud Storage (GCS), TUS v1.0.0 resumable uploads, on-the-fly image transformation, a multi-backend registry, and frontend storage sources.
9
9
 
10
10
  > **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` skill first before using this skill. Storage requires a running Rebase backend with `initializeRebaseBackend()`.
11
11
 
@@ -15,7 +15,7 @@ The `storage` option in `initializeRebaseBackend()` accepts three forms:
15
15
 
16
16
  | Form | Type | Description |
17
17
  |------|------|-------------|
18
- | Single config | `BackendStorageConfig` | `{ type: 'local' | 's3', ... }` — creates a single `(default)` backend |
18
+ | Single config | `BackendStorageConfig` | `{ type: 'local' | 's3' | 'gcs', ... }` — creates a single `(default)` backend |
19
19
  | Single controller | `StorageController` | A custom controller instance — registered as `(default)` |
20
20
  | Multi-backend map | `Record<string, BackendStorageConfig \| StorageController>` | Named backends, first becomes `(default)` if no `(default)` key |
21
21
 
@@ -44,6 +44,19 @@ The `storage` option in `initializeRebaseBackend()` accepts three forms:
44
44
  | `allowedMimeTypes` | `string[]` | `undefined` (all allowed) | Restrict uploads to these MIME types |
45
45
  | `signedUrlExpiration` | `number` | `3600` | Signed URL expiry in seconds |
46
46
 
47
+ ### GCSStorageConfig
48
+
49
+ | Property | Type | Default | Description |
50
+ |----------|------|---------|-------------|
51
+ | `type` | `"gcs"` | — | **Required.** Must be `"gcs"` |
52
+ | `bucket` | `string` | — | **Required.** GCS bucket name |
53
+ | `projectId` | `string` | `undefined` | Google Cloud project ID (auto-detected from credentials if omitted) |
54
+ | `maxFileSize` | `number` | `52428800` (50 MB) | Maximum file size in bytes |
55
+ | `allowedMimeTypes` | `string[]` | `undefined` (all allowed) | Restrict uploads to these MIME types |
56
+ | `signedUrlExpiration` | `number` | `3600` | Signed URL expiry in seconds |
57
+
58
+ > **IMPORTANT FOR AGENTS:** GCS requires `@google-cloud/storage` as a peer dependency. Install it: `pnpm add @google-cloud/storage`. Authentication uses Application Default Credentials — set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to point to a service account key file, or rely on the default credentials when running on GCP.
59
+
47
60
  ### StorageRoutesConfig
48
61
 
49
62
  These options are set internally when mounting routes but are derived from the backend config:
@@ -93,7 +106,7 @@ Local storage includes **path traversal protection** — any resolved path that
93
106
 
94
107
  ### S3-Compatible Storage
95
108
 
96
- Works with AWS S3, MinIO, DigitalOcean Spaces, Cloudflare R2, Hetzner Object Storage, Backblaze B2, and any S3-compatible service. GCS also works via its S3-compatible interoperability API.
109
+ Works with AWS S3, MinIO, DigitalOcean Spaces, Cloudflare R2, Hetzner Object Storage, Backblaze B2, and any S3-compatible service.
97
110
 
98
111
  ```typescript
99
112
  const backend = await initializeRebaseBackend({
@@ -118,6 +131,28 @@ The S3 controller:
118
131
  - Supports `s3://` and `gs://` URL schemes in key parameters for backward compatibility
119
132
  - Flattens nested metadata to string values (S3 requirement)
120
133
 
134
+ ### Google Cloud Storage (GCS)
135
+ Native Google Cloud Storage support via the `@google-cloud/storage` SDK. This is the preferred approach for GCS — no S3 interop layer needed. (Also works with Firebase Storage buckets, which are GCS buckets under the hood).
136
+
137
+ ```typescript
138
+ const backend = await initializeRebaseBackend({
139
+ server, app,
140
+ bootstrappers: [/* ... */],
141
+ storage: {
142
+ type: "gcs",
143
+ bucket: process.env.GCS_BUCKET!,
144
+ projectId: process.env.GCS_PROJECT_ID, // optional, auto-detected on GCP
145
+ },
146
+ });
147
+ ```
148
+
149
+ The GCS controller:
150
+ - Uses `@google-cloud/storage` natively (no S3 interop overhead)
151
+ - Authenticates via Application Default Credentials (`GOOGLE_APPLICATION_CREDENTIALS`)
152
+ - Maps the logical bucket name `"default"` to the configured GCS bucket
153
+ - Supports `gs://` URL scheme in key parameters
154
+ - Generates signed URLs using GCS's native signed URL mechanism
155
+
121
156
  ### Multiple Storage Backends
122
157
 
123
158
  Rebase supports multiple storage backends simultaneously via the `StorageRegistry`:
@@ -138,7 +173,7 @@ const backend = await initializeRebaseBackend({
138
173
  });
139
174
  ```
140
175
 
141
- > **IMPORTANT FOR AGENTS:** If no `"(default)"` key is provided, the first entry is automatically registered as the default (with a console warning). The REST API routes always use the default controller. Use `storageRegistry.get("media")` or `storageRegistry.getOrDefault("media")` to access named backends programmatically.
176
+ > **IMPORTANT FOR AGENTS:** If no `"(default)"` key is provided, the first entry is automatically registered as the default (with a console warning). The REST API routes use the default controller unless a `?storageId=<key>` query parameter is provided (see [REST API Endpoints](#rest-api-endpoints)). Use `storageRegistry.get("media")` or `storageRegistry.getOrDefault("media")` to access named backends programmatically.
142
177
 
143
178
  ### StorageRegistry API
144
179
 
@@ -156,7 +191,7 @@ The `StorageRegistry` interface:
156
191
 
157
192
  ### Custom Storage Providers
158
193
 
159
- Implement the `StorageController` interface for unsupported providers (Azure Blob, native GCS SDK, etc.):
194
+ GCS is now built-in (see [Google Cloud Storage (GCS)](#google-cloud-storage-gcs)). Implement the `StorageController` interface for other unsupported providers (Azure Blob, etc.):
160
195
 
161
196
  ```typescript
162
197
  interface StorageController {
@@ -185,7 +220,7 @@ The backend validates storage-related environment variables via a Zod schema:
185
220
 
186
221
  | Variable | Type | Default | Description |
187
222
  |----------|------|---------|-------------|
188
- | `STORAGE_TYPE` | `"local" \| "s3"` | `"local"` | Storage provider type |
223
+ | `STORAGE_TYPE` | `"local" \| "s3" \| "gcs"` | `"local"` | Storage provider type |
189
224
  | `STORAGE_PATH` | `string` | `"./uploads"` | Base path for local storage / TUS temp directory |
190
225
  | `FORCE_LOCAL_STORAGE` | `"true" \| "false"` | `false` | Suppress the production warning for local storage |
191
226
  | `S3_BUCKET` | `string` | — | S3 bucket name |
@@ -194,8 +229,12 @@ The backend validates storage-related environment variables via a Zod schema:
194
229
  | `S3_SECRET_ACCESS_KEY` | `string` | — | S3 secret access key |
195
230
  | `S3_ENDPOINT` | `string` (URL) | — | Custom S3-compatible endpoint |
196
231
  | `S3_FORCE_PATH_STYLE` | `"true" \| "false"` | — | Enable path-style URLs |
232
+ | `GCS_BUCKET` | `string` | — | GCS bucket name |
233
+ | `GCS_PROJECT_ID` | `string` | — | Google Cloud project ID (auto-detected on GCP) |
234
+ | `GOOGLE_APPLICATION_CREDENTIALS` | `string` (path) | — | Path to GCP service account key JSON file |
197
235
 
198
236
  ```env
237
+ # S3 example
199
238
  STORAGE_TYPE=s3
200
239
  S3_BUCKET=my-bucket
201
240
  S3_REGION=us-east-1
@@ -203,12 +242,35 @@ S3_ACCESS_KEY_ID=AKIA...
203
242
  S3_SECRET_ACCESS_KEY=...
204
243
  S3_ENDPOINT=https://minio.example.com # Optional: for MinIO, R2, etc.
205
244
  S3_FORCE_PATH_STYLE=true # Optional: for MinIO
245
+
246
+ # GCS example
247
+ STORAGE_TYPE=gcs
248
+ GCS_BUCKET=my-gcs-bucket
249
+ GCS_PROJECT_ID=my-gcp-project # Optional: auto-detected on GCP
250
+ GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
206
251
  ```
207
252
 
208
253
  ## REST API Endpoints
209
254
 
210
255
  All storage routes are mounted at `/api/storage` by default. Write operations require authentication when `requireAuth` is `true` (the default). Read operations also require authentication unless `publicRead` is `true`.
211
256
 
257
+ ### Multi-Backend Routing (`storageId`)
258
+
259
+ All storage endpoints accept a `?storageId=<key>` query parameter that routes the request to a specific named backend in the `StorageRegistry`. For multipart uploads (`POST /api/storage/upload`), the `storageId` can also be sent as a form field.
260
+
261
+ If `storageId` is omitted, the request is routed to the `(default)` backend.
262
+
263
+ ```
264
+ # Upload to the "media" backend
265
+ POST /api/storage/upload?storageId=media
266
+
267
+ # Download from the "media" backend
268
+ GET /api/storage/file/images/photo.jpg?storageId=media
269
+
270
+ # List files in the "media" backend
271
+ GET /api/storage/list?prefix=images/&storageId=media
272
+ ```
273
+
212
274
  ### Standard Endpoints
213
275
 
214
276
  | Method | Endpoint | Auth | Description |
@@ -241,6 +303,7 @@ Upload a file via `multipart/form-data`.
241
303
  | `file` | `File` | Yes | The file to upload |
242
304
  | `key` | `string` | No | Storage key/path. Falls back to original filename, then `"unnamed"` |
243
305
  | `bucket` | `string` | No | Target bucket name |
306
+ | `storageId` | `string` | No | Route to a named backend (alternative to `?storageId` query param) |
244
307
  | `metadata_*` | `string` | No | Custom metadata keys prefixed with `metadata_` |
245
308
 
246
309
  **Response** (201):
@@ -562,6 +625,24 @@ Transformed responses include `Cache-Control: public, max-age=31536000, immutabl
562
625
 
563
626
  The `@rebasepro/client` package provides a `StorageSource` interface via `createStorage(transport)`. These methods are available on `client.storage` (or through the React/Studio hooks).
564
627
 
628
+ All SDK methods accept an optional `storageId` parameter (passed as a query parameter) to route the request to a specific named backend:
629
+
630
+ ```typescript
631
+ // Upload to a specific backend
632
+ const result = await client.storage.putObject({
633
+ file: myFile,
634
+ key: "products/images/photo.jpg",
635
+ storageId: "media", // routes to the "media" backend
636
+ });
637
+
638
+ // Get signed URL from a specific backend
639
+ const config = await client.storage.getSignedUrl(
640
+ "products/images/photo.jpg",
641
+ "default", // bucket
642
+ { storageId: "media" }
643
+ );
644
+ ```
645
+
565
646
  ### putObject
566
647
 
567
648
  Upload a file.
@@ -662,7 +743,8 @@ const productsCollection: PostgresCollection = {
662
743
  name: "Product Image",
663
744
  type: "string",
664
745
  storage: {
665
- storagePath: "products/images",
746
+ storageSource: "external-source",
747
+ storagePath: "products/{entityId}",
666
748
  acceptedFiles: ["image/*"],
667
749
  maxSize: 5 * 1024 * 1024, // 5MB
668
750
  }
@@ -673,6 +755,7 @@ const productsCollection: PostgresCollection = {
673
755
  of: {
674
756
  type: "string",
675
757
  storage: {
758
+ storageSource: "media", // routes to the "media" backend
676
759
  storagePath: "products/documents",
677
760
  acceptedFiles: ["application/pdf", "application/msword"],
678
761
  }
@@ -682,6 +765,87 @@ const productsCollection: PostgresCollection = {
682
765
  };
683
766
  ```
684
767
 
768
+ The `storageSource` field maps to a named backend registered in the `StorageRegistry` (server-side) or in the `storageSources` prop (client-side). When omitted, the `(default)` backend is used.
769
+
770
+ ### StorageConfig Properties
771
+
772
+ | Property | Type | Description |
773
+ |----------|------|-------------|
774
+ | `storageSource` | `string` | Named backend to route uploads to (optional, defaults to `(default)`) |
775
+ | `storagePath` | `string` | Path template for uploaded files. Supports `{entityId}` placeholder |
776
+ | `acceptedFiles` | `string[]` | Accepted MIME types or globs (e.g., `["image/*"]`) |
777
+ | `maxSize` | `number` | Maximum file size in bytes |
778
+
779
+ ## Frontend Storage Sources
780
+
781
+ The `<Rebase>` component accepts a `storageSources` prop to register client-side storage sources, following the same pattern as `dataSources` for databases. This enables **direct** (client-to-provider) file operations, bypassing the Rebase backend.
782
+
783
+ ### Registering Storage Sources
784
+
785
+ ```tsx
786
+ import type { RebaseStorageSource } from "@rebasepro/core";
787
+ import { myExternalStorageSource } from "./my-external-storage";
788
+
789
+ <Rebase
790
+ client={rebaseClient}
791
+ storageSources={[
792
+ {
793
+ key: "external",
794
+ engine: "external",
795
+ transport: "direct",
796
+ source: myExternalStorageSource,
797
+ },
798
+ {
799
+ key: "media",
800
+ engine: "s3",
801
+ transport: "server", // proxied through the Rebase backend
802
+ label: "Media Storage",
803
+ },
804
+ ]}
805
+ >
806
+ {children}
807
+ </Rebase>
808
+ ```
809
+
810
+ ### StorageSourceDefinition
811
+
812
+ | Property | Type | Description |
813
+ |----------|------|-------------|
814
+ | `key` | `string` | **Required.** Unique identifier for this storage source |
815
+ | `engine` | `string` | **Required.** Storage engine type (e.g., `"firebase"`, `"s3"`, `"gcs"`, `"local"`) |
816
+ | `transport` | `"server" \| "direct"` | **Required.** `"server"` routes through the backend; `"direct"` talks to the provider from the client |
817
+ | `source` | `StorageSource` | The client-side `StorageSource` implementation (required when `transport` is `"direct"`) |
818
+ | `label` | `string` | Optional human-readable label for the UI |
819
+
820
+ ### StorageSourcesContext
821
+
822
+ Registered storage sources are exposed to the component tree via `StorageSourcesContext`, mirroring the `DataSourcesContext` pattern:
823
+
824
+ ```typescript
825
+ import { useStorageSources } from "@rebasepro/core";
826
+
827
+ function MyUploadComponent() {
828
+ const storageSources = useStorageSources();
829
+
830
+ // Get a specific source
831
+ const externalSource = storageSources.find(s => s.key === "external");
832
+
833
+ // Use it for direct uploads
834
+ if (externalSource?.source) {
835
+ await externalSource.source.putObject({ file, key: "photos/image.jpg" });
836
+ }
837
+ }
838
+ ```
839
+
840
+ ### Direct vs Server Transport
841
+
842
+ | Transport | Description | Use Case |
843
+ |-----------|-------------|----------|
844
+ | `"server"` | File operations are proxied through the Rebase backend REST API | S3, GCS, or local backends managed by the server |
845
+ | `"direct"` | File operations go directly from the client to the storage provider | External cloud storage, or any provider with client-side SDKs |
846
+
847
+ > **IMPORTANT FOR AGENTS:** Direct transport sources must provide a `source` property implementing the `StorageSource` interface. Server transport sources use the built-in SDK transport and only need `key` and `engine`.
848
+
685
849
  ## Storage Browser (Studio)
686
850
 
687
851
  The `@rebasepro/studio` package includes a built-in `StorageView` component:
@@ -848,6 +848,97 @@ Key props: `data`, `columns` (array of `VirtualTableColumn`), `cellRenderer`, `o
848
848
 
849
849
  See `VirtualTableProps` and `VirtualTableColumn` types for full API.
850
850
 
851
+ #### Generic Editable Tables & Cell Selection
852
+
853
+ `@rebasepro/ui` includes a generic, high-performance selection engine and primitive cell inputs that allow you to construct keyboard-navigable editable grid tables without any Rebase database dependencies.
854
+
855
+ **Selection Hooks & Context:**
856
+ * `createVirtualTableSelectionStore()` — Instantiates a selection store tracking active selection by `rowId` and `columnKey`. It uses `useSyncExternalStore` so that selection changes only trigger re-renders on the active cell (preventing whole-table lag).
857
+ * `VirtualTableSelectionProvider` — React context provider to pass down the selection store.
858
+ * `useVirtualTableSelection()` — Hook to access the selection store and triggers inside custom cells.
859
+ * `useVirtualTableCellSelected(store, columnKey, rowId)` — Returns `boolean` indicating if this cell is currently focused.
860
+
861
+ **Generic Cell Inputs:**
862
+ * `VirtualTableInput` — Auto-growing, debounced multiline/single text input.
863
+ * `VirtualTableNumberInput` — Constrained numeric input.
864
+ * `VirtualTableSwitch` — Small boolean switch toggle.
865
+ * `VirtualTableDateField` — Decoupled Date/DateTime picker (accepts `locale`, `timezone`, `mode`).
866
+ * `VirtualTableSelect` — Option-based single/multi dropdown list using chips. Takes `options: { value, label, color? }[]`.
867
+
868
+ **Example Custom Editable Table:**
869
+ ```tsx
870
+ import React, { useMemo, useState, useCallback } from "react";
871
+ import {
872
+ VirtualTable,
873
+ createVirtualTableSelectionStore,
874
+ VirtualTableSelectionProvider,
875
+ useVirtualTableCellSelected,
876
+ useVirtualTableSelection,
877
+ VirtualTableInput
878
+ } from "@rebasepro/ui";
879
+
880
+ function EditableCell({ rowId, columnKey, value, onUpdate }) {
881
+ const { selectionStore } = useVirtualTableSelection();
882
+ const selected = useVirtualTableCellSelected(selectionStore, columnKey, rowId);
883
+
884
+ return (
885
+ <div
886
+ className="w-full h-full flex items-center px-2 cursor-text"
887
+ onClick={(e) => selectionStore.select({
888
+ rowId,
889
+ columnKey,
890
+ cellRect: e.currentTarget.getBoundingClientRect(),
891
+ width: e.currentTarget.offsetWidth,
892
+ height: e.currentTarget.offsetHeight
893
+ })}
894
+ >
895
+ <VirtualTableInput
896
+ value={value}
897
+ updateValue={onUpdate}
898
+ focused={selected}
899
+ disabled={false}
900
+ multiline={false}
901
+ />
902
+ </div>
903
+ );
904
+ }
905
+
906
+ export function MyEditableGrid() {
907
+ const [data, setData] = useState([
908
+ { id: "row-1", name: "Project Alpha" },
909
+ { id: "row-2", name: "Project Beta" }
910
+ ]);
911
+
912
+ const selectionStore = useMemo(() => createVirtualTableSelectionStore(), []);
913
+
914
+ const updateCell = useCallback((rowId: string, value: string) => {
915
+ setData(prev => prev.map(row => row.id === rowId ? { ...row, name: value } : row));
916
+ }, []);
917
+
918
+ const columns = [
919
+ { key: "name", title: "Name", width: 300 }
920
+ ];
921
+
922
+ return (
923
+ <VirtualTableSelectionProvider store={selectionStore}>
924
+ <VirtualTable
925
+ data={data}
926
+ columns={columns}
927
+ rowHeight={52}
928
+ cellRenderer={({ rowData, column }) => (
929
+ <EditableCell
930
+ rowId={rowData.id}
931
+ columnKey={column.key}
932
+ value={rowData.name}
933
+ onUpdate={(val) => updateCell(rowData.id, val || "")}
934
+ />
935
+ )}
936
+ />
937
+ </VirtualTableSelectionProvider>
938
+ );
939
+ }
940
+ ```
941
+
851
942
  ### ToggleButtonGroup
852
943
 
853
944
  Single-select button group for toggling between options.