@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
|
@@ -17,7 +17,7 @@ Rebase implements a **multi-layered, defense-in-depth** security architecture. S
|
|
|
17
17
|
- [Layer 2: API Key Permission Guard](#layer-2-api-key-permission-guard)
|
|
18
18
|
- [Layer 3: DataHooks (API Boundary)](#layer-3-datahooks-api-boundary)
|
|
19
19
|
- [Layer 4: Scoped DataDriver](#layer-4-scoped-datadriver)
|
|
20
|
-
- [Layer 5:
|
|
20
|
+
- [Layer 5: Collection Callbacks](#layer-5-collection-callbacks)
|
|
21
21
|
- [Fail-Closed Design](#fail-closed-design)
|
|
22
22
|
- [Securing Without Database RLS](#securing-without-database-rls)
|
|
23
23
|
- [Common Security Patterns](#common-security-patterns)
|
|
@@ -61,7 +61,7 @@ Every request — REST, GraphQL, and WebSocket — passes through **5 security l
|
|
|
61
61
|
└──────────────┬───────────────────────────────────────┘
|
|
62
62
|
│
|
|
63
63
|
┌──────────────▼───────────────────────────────────────┐
|
|
64
|
-
│ Layer 5:
|
|
64
|
+
│ Layer 5: Collection Callbacks (Per-Collection) │
|
|
65
65
|
│ beforeSave / afterRead / beforeDelete │
|
|
66
66
|
│ Per-collection hooks inside the DataDriver │
|
|
67
67
|
└──────────────────────────────────────────────────────┘
|
|
@@ -77,12 +77,12 @@ All three protocols share the same security middleware stack:
|
|
|
77
77
|
|
|
78
78
|
### REST
|
|
79
79
|
```
|
|
80
|
-
HTTP Request → Auth Middleware → API Key Guard → DataHooks → Scoped Driver →
|
|
80
|
+
HTTP Request → Auth Middleware → API Key Guard → DataHooks → Scoped Driver → Collection Callbacks → Response
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
### GraphQL
|
|
84
84
|
```
|
|
85
|
-
GraphQL Request → Auth Middleware → API Key Guard → Scoped Driver →
|
|
85
|
+
GraphQL Request → Auth Middleware → API Key Guard → Scoped Driver → Collection Callbacks → Response
|
|
86
86
|
```
|
|
87
87
|
GraphQL shares the same Hono middleware chain as REST. Resolvers extract the scoped driver from context and throw if unavailable.
|
|
88
88
|
|
|
@@ -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 Collection 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 Collection 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 Collection 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
|
|
@@ -239,9 +247,9 @@ interface BackendHookContext {
|
|
|
239
247
|
|
|
240
248
|
### Execution Order
|
|
241
249
|
|
|
242
|
-
DataHooks run **after** per-collection `
|
|
250
|
+
DataHooks run **after** per-collection `CollectionCallbacks` (which execute inside the DataDriver, closer to the database) and **before** the API response is sent to the client. This gives you two opportunities to enforce security:
|
|
243
251
|
|
|
244
|
-
1. **
|
|
252
|
+
1. **CollectionCallbacks** — per-collection, inside the driver
|
|
245
253
|
2. **DataHooks** — cross-cutting, at the API boundary
|
|
246
254
|
|
|
247
255
|
---
|
|
@@ -263,12 +271,12 @@ PostgreSQL RLS policies use `auth.uid()`, `auth.roles()`, and `auth.jwt()` to re
|
|
|
263
271
|
|
|
264
272
|
---
|
|
265
273
|
|
|
266
|
-
## Layer 5:
|
|
274
|
+
## Layer 5: Collection Callbacks
|
|
267
275
|
|
|
268
|
-
|
|
276
|
+
Collection callbacks are per-collection lifecycle hooks that run **inside** the DataDriver, close to the database. They provide collection-specific security enforcement:
|
|
269
277
|
|
|
270
278
|
```typescript
|
|
271
|
-
const ordersCollection:
|
|
279
|
+
const ordersCollection: PostgresCollectionConfig = {
|
|
272
280
|
name: "Orders",
|
|
273
281
|
slug: "orders",
|
|
274
282
|
table: "orders",
|
|
@@ -281,9 +289,9 @@ const ordersCollection: PostgresCollection = {
|
|
|
281
289
|
}
|
|
282
290
|
return values;
|
|
283
291
|
},
|
|
284
|
-
beforeDelete: async ({
|
|
292
|
+
beforeDelete: async ({ row, context }) => {
|
|
285
293
|
// Prevent deletion of fulfilled orders
|
|
286
|
-
if (
|
|
294
|
+
if (row.status === "fulfilled") {
|
|
287
295
|
throw new Error("Cannot delete fulfilled orders");
|
|
288
296
|
}
|
|
289
297
|
},
|
|
@@ -292,7 +300,7 @@ const ordersCollection: PostgresCollection = {
|
|
|
292
300
|
};
|
|
293
301
|
```
|
|
294
302
|
|
|
295
|
-
For full documentation on
|
|
303
|
+
For full documentation on collection callbacks, see the `rebase-collections` skill.
|
|
296
304
|
|
|
297
305
|
---
|
|
298
306
|
|
|
@@ -319,7 +327,7 @@ Rebase follows a **fail-closed** security model throughout the stack:
|
|
|
319
327
|
|
|
320
328
|
## Securing Without Database RLS
|
|
321
329
|
|
|
322
|
-
If you cannot modify database-level RLS policies — or your database doesn't support them — use **DataHooks** and **
|
|
330
|
+
If you cannot modify database-level RLS policies — or your database doesn't support them — use **DataHooks** and **Collection Callbacks** to enforce security entirely at the application level.
|
|
323
331
|
|
|
324
332
|
### Strategy: DataHooks as Your Security Layer
|
|
325
333
|
|
|
@@ -376,7 +384,7 @@ const hooks: BackendHooks = {
|
|
|
376
384
|
|
|
377
385
|
if (!user.roles.includes("admin")) {
|
|
378
386
|
// For non-admins, you may need to fetch the entity first
|
|
379
|
-
// to verify ownership. Use
|
|
387
|
+
// to verify ownership. Use Collection Callbacks for this pattern
|
|
380
388
|
// since they receive the full entity.
|
|
381
389
|
throw ApiError.forbidden("Only admins can delete records");
|
|
382
390
|
}
|
|
@@ -385,40 +393,40 @@ const hooks: BackendHooks = {
|
|
|
385
393
|
};
|
|
386
394
|
```
|
|
387
395
|
|
|
388
|
-
### Strategy:
|
|
396
|
+
### Strategy: Collection Callbacks for Ownership Checks
|
|
389
397
|
|
|
390
|
-
|
|
398
|
+
Collection Callbacks receive the full entity data, making them ideal for ownership verification on deletes and updates:
|
|
391
399
|
|
|
392
400
|
```typescript
|
|
393
|
-
const ordersCollection:
|
|
401
|
+
const ordersCollection: PostgresCollectionConfig = {
|
|
394
402
|
name: "Orders",
|
|
395
403
|
slug: "orders",
|
|
396
404
|
table: "orders",
|
|
397
405
|
callbacks: {
|
|
398
|
-
beforeSave: async ({ values,
|
|
406
|
+
beforeSave: async ({ values, id, context, previousValues }) => {
|
|
399
407
|
const user = context.user;
|
|
400
408
|
if (!user) throw new Error("Unauthorized");
|
|
401
409
|
|
|
402
410
|
// On update, verify the current user owns this order
|
|
403
|
-
if (
|
|
411
|
+
if (id && previousValues) {
|
|
404
412
|
if (previousValues.user_id !== user.uid && !user.roles?.includes("admin")) {
|
|
405
413
|
throw new Error("You can only edit your own orders");
|
|
406
414
|
}
|
|
407
415
|
}
|
|
408
416
|
|
|
409
417
|
// On create, stamp ownership
|
|
410
|
-
if (!
|
|
418
|
+
if (!id) {
|
|
411
419
|
values.user_id = user.uid;
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
return values;
|
|
415
423
|
},
|
|
416
424
|
|
|
417
|
-
beforeDelete: async ({
|
|
425
|
+
beforeDelete: async ({ row, context }) => {
|
|
418
426
|
const user = context.user;
|
|
419
427
|
if (!user) throw new Error("Unauthorized");
|
|
420
428
|
|
|
421
|
-
if (
|
|
429
|
+
if (row.user_id !== user.uid && !user.roles?.includes("admin")) {
|
|
422
430
|
throw new Error("You can only delete your own orders");
|
|
423
431
|
}
|
|
424
432
|
},
|
|
@@ -429,15 +437,15 @@ const ordersCollection: PostgresCollection = {
|
|
|
429
437
|
|
|
430
438
|
### Combining Both Layers
|
|
431
439
|
|
|
432
|
-
For maximum security, use both DataHooks and
|
|
440
|
+
For maximum security, use both DataHooks and Collection Callbacks together:
|
|
433
441
|
|
|
434
442
|
| Concern | Best Layer | Why |
|
|
435
443
|
|---|---|---|
|
|
436
444
|
| Cross-cutting read filtering | DataHooks `afterRead` | Applies to ALL collections in one place |
|
|
437
445
|
| Cross-cutting write validation | DataHooks `beforeSave` | Single enforcement point for all writes |
|
|
438
446
|
| PII masking / field redaction | DataHooks `afterRead` | Cross-cutting, role-based |
|
|
439
|
-
| Ownership checks on writes/deletes |
|
|
440
|
-
| Business rule validation |
|
|
447
|
+
| Ownership checks on writes/deletes | Collection Callbacks | Has access to the full entity for comparison |
|
|
448
|
+
| Business rule validation | Collection Callbacks | Collection-specific, typed values |
|
|
441
449
|
| Audit logging | DataHooks `afterSave` / `afterDelete` | Cross-cutting, fire-and-forget |
|
|
442
450
|
|
|
443
451
|
---
|
|
@@ -495,13 +503,44 @@ const hooks: BackendHooks = {
|
|
|
495
503
|
},
|
|
496
504
|
|
|
497
505
|
beforeDelete(slug, entityId, ctx) {
|
|
498
|
-
// Tenant isolation on deletes is best handled in
|
|
506
|
+
// Tenant isolation on deletes is best handled in Collection Callbacks
|
|
499
507
|
// where you have access to the entity's tenant_id
|
|
500
508
|
},
|
|
501
509
|
},
|
|
502
510
|
};
|
|
503
511
|
```
|
|
504
512
|
|
|
513
|
+
### Membership-Scoped Access (RLS, no N+1)
|
|
514
|
+
|
|
515
|
+
When membership lives in a **join collection** (e.g. `team_members`), prefer the
|
|
516
|
+
first-class `policy.existsIn` predicate over a per-row `afterRead` lookup. It is
|
|
517
|
+
enforced by Postgres RLS in a single correlated `EXISTS` subquery — no N+1, and
|
|
518
|
+
it cannot be bypassed by a client that skips the SDK.
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { policy } from "@rebasepro/types";
|
|
522
|
+
|
|
523
|
+
// config/collections/documents.ts — only members of the doc's team can read it:
|
|
524
|
+
securityRules: [
|
|
525
|
+
{
|
|
526
|
+
operation: "select",
|
|
527
|
+
condition: policy.existsIn({
|
|
528
|
+
collection: "team_members",
|
|
529
|
+
where: policy.and(
|
|
530
|
+
policy.compare(policy.field("team_id"), "eq", policy.outerField("team_id")),
|
|
531
|
+
policy.compare(policy.field("user_id"), "eq", policy.authUid()),
|
|
532
|
+
),
|
|
533
|
+
}),
|
|
534
|
+
},
|
|
535
|
+
]
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Inside `where`: `policy.field(...)` = a column of the joined collection
|
|
539
|
+
(`team_members`); `policy.outerField(...)` = a column of the row being checked
|
|
540
|
+
(`documents`); `policy.authUid()` = the caller. Reach for the `afterRead`
|
|
541
|
+
approach above only when access depends on data RLS can't see (e.g. an external
|
|
542
|
+
service). Run `rebase db push` after editing the collection to apply the policy.
|
|
543
|
+
|
|
505
544
|
### Role-Based Collection Access
|
|
506
545
|
|
|
507
546
|
```typescript
|
|
@@ -585,7 +624,7 @@ Use this checklist when setting up security for a Rebase project:
|
|
|
585
624
|
- [ ] **Default role is NOT admin** — `auth.defaultRole` must never be `"admin"` (startup error)
|
|
586
625
|
- [ ] **DataHooks enforce access control** — If not using database RLS, `hooks.data` enforces read/write/delete permissions
|
|
587
626
|
- [ ] **Sensitive fields are masked** — `afterRead` masks PII for non-admin users
|
|
588
|
-
- [ ] **Ownership is enforced** — `beforeSave` stamps `user_id` on creation;
|
|
627
|
+
- [ ] **Ownership is enforced** — `beforeSave` stamps `user_id` on creation; Collection Callbacks verify ownership on update/delete
|
|
589
628
|
- [ ] **API keys are scoped** — API keys have minimal permissions (specific collections + operations)
|
|
590
629
|
- [ ] **API keys are never client-side** — API keys bypass RLS; only use server-side
|
|
591
630
|
- [ ] **CORS is configured** — Restrict origins in production
|
|
@@ -602,5 +641,6 @@ Use this checklist when setting up security for a Rebase project:
|
|
|
602
641
|
- **REST API Generator**: `packages/server-core/src/api/rest/api-generator.ts` — DataHooks integration
|
|
603
642
|
- **Backend Hooks Types**: `packages/types/src/types/backend_hooks.ts` — `DataHooks`, `BackendHooks` interfaces
|
|
604
643
|
- **Backend Init**: `packages/server-core/src/init.ts` — `hooks` config property
|
|
605
|
-
- **
|
|
644
|
+
- **Reserved Identity Values**: See Identity Types table above — `"service"`, `"anon"`, `"api-key:{id}"` are system-assigned identities in `context.user` / `ctx.requestUser`
|
|
645
|
+
- **Collection Callbacks**: See `rebase-collections` skill → Collection Callbacks section
|
|
606
646
|
- **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
|
|
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.
|
|
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
|
|
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,
|
|
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.
|
|
@@ -591,12 +672,15 @@ const config = await client.storage.getSignedUrl(
|
|
|
591
672
|
if (config.fileNotFound) {
|
|
592
673
|
console.log("File does not exist");
|
|
593
674
|
} else {
|
|
594
|
-
console.log(config.url); //
|
|
675
|
+
console.log(config.url); // Private: short-lived scoped token appended. Public: clean permanent URL.
|
|
595
676
|
console.log(config.metadata); // { bucket, fullPath, name, size, contentType, customMetadata }
|
|
596
677
|
}
|
|
597
678
|
```
|
|
598
679
|
|
|
599
|
-
|
|
680
|
+
**URL model — never contains the access token:**
|
|
681
|
+
|
|
682
|
+
- **Private files** → `{baseUrl}/api/storage/file/{path}?token={scopedDownloadToken}`. The token is a **short-lived, path-scoped** download token (default 5 min), signed by the server and valid only for that object — never the caller's full access JWT. Because it expires, **do not persist a private URL**: store the file **path** and call `getSignedUrl()` again at render time.
|
|
683
|
+
- **Public files** → `{baseUrl}/api/storage/file/{path}` with **no token**. A file is public when it lives under the `public/` prefix — set `storage: { public: true }` on the property, or pass `public: true` to `putObject`. Public URLs are **stable, permanent, and CDN-cacheable**, so they are safe to store in a database and hotlink. For public paths, `getSignedUrl()` returns the URL immediately with no server round-trip.
|
|
600
684
|
|
|
601
685
|
Protocol prefixes (`local://`, `s3://`) are automatically stripped from the key.
|
|
602
686
|
|
|
@@ -652,9 +736,9 @@ console.log(result.nextPageToken); // for next page, or undefined
|
|
|
652
736
|
Define file upload fields in your collections using the `storage` option on string properties:
|
|
653
737
|
|
|
654
738
|
```typescript
|
|
655
|
-
import {
|
|
739
|
+
import { PostgresCollectionConfig } from "@rebasepro/types";
|
|
656
740
|
|
|
657
|
-
const productsCollection:
|
|
741
|
+
const productsCollection: PostgresCollectionConfig = {
|
|
658
742
|
name: "Products",
|
|
659
743
|
table: "products",
|
|
660
744
|
properties: {
|
|
@@ -662,7 +746,8 @@ const productsCollection: PostgresCollection = {
|
|
|
662
746
|
name: "Product Image",
|
|
663
747
|
type: "string",
|
|
664
748
|
storage: {
|
|
665
|
-
|
|
749
|
+
storageSource: "external-source",
|
|
750
|
+
storagePath: "products/{entityId}",
|
|
666
751
|
acceptedFiles: ["image/*"],
|
|
667
752
|
maxSize: 5 * 1024 * 1024, // 5MB
|
|
668
753
|
}
|
|
@@ -673,6 +758,7 @@ const productsCollection: PostgresCollection = {
|
|
|
673
758
|
of: {
|
|
674
759
|
type: "string",
|
|
675
760
|
storage: {
|
|
761
|
+
storageSource: "media", // routes to the "media" backend
|
|
676
762
|
storagePath: "products/documents",
|
|
677
763
|
acceptedFiles: ["application/pdf", "application/msword"],
|
|
678
764
|
}
|
|
@@ -682,6 +768,88 @@ const productsCollection: PostgresCollection = {
|
|
|
682
768
|
};
|
|
683
769
|
```
|
|
684
770
|
|
|
771
|
+
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.
|
|
772
|
+
|
|
773
|
+
### StorageConfig Properties
|
|
774
|
+
|
|
775
|
+
| Property | Type | Description |
|
|
776
|
+
|----------|------|-------------|
|
|
777
|
+
| `storageSource` | `string` | Named backend to route uploads to (optional, defaults to `(default)`) |
|
|
778
|
+
| `storagePath` | `string` | Path template for uploaded files. Supports `{entityId}` placeholder |
|
|
779
|
+
| `acceptedFiles` | `string[]` | Accepted MIME types or globs (e.g., `["image/*"]`) |
|
|
780
|
+
| `maxSize` | `number` | Maximum file size in bytes |
|
|
781
|
+
| `public` | `boolean` | Store files under the `public/` prefix and serve them via stable, token-less, permanent, CDN-cacheable URLs (safe to persist/hotlink). Defaults to `false` (private, short-lived signed URLs). |
|
|
782
|
+
|
|
783
|
+
## Frontend Storage Sources
|
|
784
|
+
|
|
785
|
+
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.
|
|
786
|
+
|
|
787
|
+
### Registering Storage Sources
|
|
788
|
+
|
|
789
|
+
```tsx
|
|
790
|
+
import type { RebaseStorageSource } from "@rebasepro/core";
|
|
791
|
+
import { myExternalStorageSource } from "./my-external-storage";
|
|
792
|
+
|
|
793
|
+
<Rebase
|
|
794
|
+
client={rebaseClient}
|
|
795
|
+
storageSources={[
|
|
796
|
+
{
|
|
797
|
+
key: "external",
|
|
798
|
+
engine: "external",
|
|
799
|
+
transport: "direct",
|
|
800
|
+
source: myExternalStorageSource,
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
key: "media",
|
|
804
|
+
engine: "s3",
|
|
805
|
+
transport: "server", // proxied through the Rebase backend
|
|
806
|
+
label: "Media Storage",
|
|
807
|
+
},
|
|
808
|
+
]}
|
|
809
|
+
>
|
|
810
|
+
{children}
|
|
811
|
+
</Rebase>
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### StorageSourceDefinition
|
|
815
|
+
|
|
816
|
+
| Property | Type | Description |
|
|
817
|
+
|----------|------|-------------|
|
|
818
|
+
| `key` | `string` | **Required.** Unique identifier for this storage source |
|
|
819
|
+
| `engine` | `string` | **Required.** Storage engine type (e.g., `"firebase"`, `"s3"`, `"gcs"`, `"local"`) |
|
|
820
|
+
| `transport` | `"server" \| "direct"` | **Required.** `"server"` routes through the backend; `"direct"` talks to the provider from the client |
|
|
821
|
+
| `source` | `StorageSource` | The client-side `StorageSource` implementation (required when `transport` is `"direct"`) |
|
|
822
|
+
| `label` | `string` | Optional human-readable label for the UI |
|
|
823
|
+
|
|
824
|
+
### StorageSourcesContext
|
|
825
|
+
|
|
826
|
+
Registered storage sources are exposed to the component tree via `StorageSourcesContext`, mirroring the `DataSourcesContext` pattern:
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
import { useStorageSources } from "@rebasepro/core";
|
|
830
|
+
|
|
831
|
+
function MyUploadComponent() {
|
|
832
|
+
const storageSources = useStorageSources();
|
|
833
|
+
|
|
834
|
+
// Get a specific source
|
|
835
|
+
const externalSource = storageSources.find(s => s.key === "external");
|
|
836
|
+
|
|
837
|
+
// Use it for direct uploads
|
|
838
|
+
if (externalSource?.source) {
|
|
839
|
+
await externalSource.source.putObject({ file, key: "photos/image.jpg" });
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### Direct vs Server Transport
|
|
845
|
+
|
|
846
|
+
| Transport | Description | Use Case |
|
|
847
|
+
|-----------|-------------|----------|
|
|
848
|
+
| `"server"` | File operations are proxied through the Rebase backend REST API | S3, GCS, or local backends managed by the server |
|
|
849
|
+
| `"direct"` | File operations go directly from the client to the storage provider | External cloud storage, or any provider with client-side SDKs |
|
|
850
|
+
|
|
851
|
+
> **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`.
|
|
852
|
+
|
|
685
853
|
## Storage Browser (Studio)
|
|
686
854
|
|
|
687
855
|
The `@rebasepro/studio` package includes a built-in `StorageView` component:
|
|
@@ -256,8 +256,8 @@ These components can be overridden globally on `<Rebase>` (which acts as a fallb
|
|
|
256
256
|
`<RebaseCMS>` accepts the full `RebaseCMSConfig`:
|
|
257
257
|
|
|
258
258
|
```typescript
|
|
259
|
-
interface RebaseCMSConfig<EC extends
|
|
260
|
-
collections?: EC[] |
|
|
259
|
+
interface RebaseCMSConfig<EC extends CollectionConfig = CollectionConfig> {
|
|
260
|
+
collections?: EC[] | CollectionConfigsBuilder<EC>;
|
|
261
261
|
views?: AppView[] | AppViewsBuilder;
|
|
262
262
|
homePage?: ReactNode;
|
|
263
263
|
entityViews?: EntityCustomView[];
|
|
@@ -464,7 +464,7 @@ type CollectionPanelProps = {
|
|
|
464
464
|
updateUrl?: boolean; // Sync filter/sort with URL (default: false)
|
|
465
465
|
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
466
466
|
className?: string; // Container CSS class
|
|
467
|
-
collectionOverrides?: Partial<
|
|
467
|
+
collectionOverrides?: Partial<CollectionConfig>; // Additional overrides
|
|
468
468
|
};
|
|
469
469
|
```
|
|
470
470
|
|
|
@@ -512,7 +512,7 @@ The Studio Bridge provides CMS capabilities to Studio components. When CMS is pr
|
|
|
512
512
|
```typescript
|
|
513
513
|
interface StudioBridge {
|
|
514
514
|
collectionRegistry: CollectionRegistryController;
|
|
515
|
-
sideEntityController:
|
|
515
|
+
sideEntityController: SidePanelController;
|
|
516
516
|
urlController: UrlController;
|
|
517
517
|
navigationState: NavigationStateController;
|
|
518
518
|
breadcrumbs: BreadcrumbsController;
|
|
@@ -524,7 +524,7 @@ interface StudioBridge {
|
|
|
524
524
|
| Hook | Return Type | Description |
|
|
525
525
|
|------|-------------|-------------|
|
|
526
526
|
| `useStudioCollectionRegistry()` | `CollectionRegistryController` | Access registered collections from Studio |
|
|
527
|
-
| `
|
|
527
|
+
| `useStudioSidePanelController()` | `SidePanelController` | Open/close entity side panels from Studio |
|
|
528
528
|
| `useStudioUrlController()` | `UrlController` | Build URLs and navigate from Studio |
|
|
529
529
|
| `useStudioNavigationState()` | `NavigationStateController` | Access navigation state from Studio |
|
|
530
530
|
| `useStudioBreadcrumbs()` | `BreadcrumbsController` | Set breadcrumbs from Studio tools |
|
|
@@ -534,7 +534,7 @@ All bridge hooks are exported from `@rebasepro/studio` (re-exported from `@rebas
|
|
|
534
534
|
```typescript
|
|
535
535
|
import {
|
|
536
536
|
useStudioCollectionRegistry,
|
|
537
|
-
|
|
537
|
+
useStudioSidePanelController,
|
|
538
538
|
useStudioUrlController,
|
|
539
539
|
useStudioNavigationState,
|
|
540
540
|
useStudioBreadcrumbs
|
|
@@ -567,7 +567,7 @@ import { StudioBridgeProvider } from "@rebasepro/studio";
|
|
|
567
567
|
|
|
568
568
|
<StudioBridgeProvider value={{
|
|
569
569
|
collectionRegistry: useCollectionRegistryController(),
|
|
570
|
-
sideEntityController:
|
|
570
|
+
sideEntityController: useSidePanel(),
|
|
571
571
|
urlController: useUrlController(),
|
|
572
572
|
navigationState: useNavigationStateController(),
|
|
573
573
|
breadcrumbs: useBreadcrumbsController(),
|
|
@@ -610,7 +610,7 @@ These hooks are exported from `@rebasepro/core` and available inside any `<Rebas
|
|
|
610
610
|
|
|
611
611
|
| Hook | Package | Description |
|
|
612
612
|
|------|---------|-------------|
|
|
613
|
-
| `
|
|
613
|
+
| `useSidePanel()` | `@rebasepro/admin` | Open/close entity side panels |
|
|
614
614
|
| `useNavigationStateController()` | `@rebasepro/admin` | Navigate between views |
|
|
615
615
|
| `useUrlController()` | `@rebasepro/admin` | Build URLs and navigate |
|
|
616
616
|
| `useBreadcrumbsController()` | `@rebasepro/admin` | Set breadcrumbs |
|