@rebasepro/agent-skills 0.0.1-canary.4829d6e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/package.json +21 -0
  4. package/skills/rebase-admin/SKILL.md +816 -0
  5. package/skills/rebase-api/SKILL.md +673 -0
  6. package/skills/rebase-api/references/.gitkeep +3 -0
  7. package/skills/rebase-auth/SKILL.md +1323 -0
  8. package/skills/rebase-auth/references/.gitkeep +3 -0
  9. package/skills/rebase-backend-postgres/SKILL.md +633 -0
  10. package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
  11. package/skills/rebase-basics/SKILL.md +757 -0
  12. package/skills/rebase-basics/references/.gitkeep +3 -0
  13. package/skills/rebase-collections/SKILL.md +1421 -0
  14. package/skills/rebase-collections/references/.gitkeep +3 -0
  15. package/skills/rebase-cron-jobs/SKILL.md +704 -0
  16. package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
  17. package/skills/rebase-custom-functions/SKILL.md +281 -0
  18. package/skills/rebase-deployment/SKILL.md +894 -0
  19. package/skills/rebase-deployment/references/.gitkeep +3 -0
  20. package/skills/rebase-design-language/SKILL.md +692 -0
  21. package/skills/rebase-email/SKILL.md +701 -0
  22. package/skills/rebase-email/references/.gitkeep +1 -0
  23. package/skills/rebase-entity-history/SKILL.md +485 -0
  24. package/skills/rebase-entity-history/references/.gitkeep +1 -0
  25. package/skills/rebase-local-env-setup/SKILL.md +189 -0
  26. package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
  27. package/skills/rebase-realtime/SKILL.md +759 -0
  28. package/skills/rebase-realtime/references/.gitkeep +3 -0
  29. package/skills/rebase-sdk/SKILL.md +617 -0
  30. package/skills/rebase-sdk/references/.gitkeep +0 -0
  31. package/skills/rebase-security/SKILL.md +646 -0
  32. package/skills/rebase-storage/SKILL.md +989 -0
  33. package/skills/rebase-storage/references/.gitkeep +3 -0
  34. package/skills/rebase-studio/SKILL.md +772 -0
  35. package/skills/rebase-studio/references/.gitkeep +3 -0
  36. package/skills/rebase-ui-components/SKILL.md +1579 -0
  37. package/skills/rebase-ui-components/references/.gitkeep +3 -0
  38. package/skills/rebase-webhooks/SKILL.md +618 -0
  39. package/skills/rebase-webhooks/references/.gitkeep +1 -0
@@ -0,0 +1,989 @@
1
+ ---
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, 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
+ ---
5
+
6
+ # Rebase Storage
7
+
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
+
10
+ > **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` skill first before using this skill. Storage requires a running Rebase backend with `initializeRebaseBackend()`.
11
+
12
+ ## Storage Configuration
13
+
14
+ The `storage` option in `initializeRebaseBackend()` accepts three forms:
15
+
16
+ | Form | Type | Description |
17
+ |------|------|-------------|
18
+ | Single config | `BackendStorageConfig` | `{ type: 'local' | 's3' | 'gcs', ... }` — creates a single `(default)` backend |
19
+ | Single controller | `StorageController` | A custom controller instance — registered as `(default)` |
20
+ | Multi-backend map | `Record<string, BackendStorageConfig \| StorageController>` | Named backends, first becomes `(default)` if no `(default)` key |
21
+
22
+ ### LocalStorageConfig
23
+
24
+ | Property | Type | Default | Description |
25
+ |----------|------|---------|-------------|
26
+ | `type` | `"local"` | — | **Required.** Must be `"local"` |
27
+ | `basePath` | `string` | — | **Required.** Base directory for file storage (e.g., `"./uploads"`) |
28
+ | `maxFileSize` | `number` | `52428800` (50 MB) | Maximum file size in bytes |
29
+ | `allowedMimeTypes` | `string[]` | `undefined` (all allowed) | Restrict uploads to these MIME types |
30
+ | `baseUrl` | `string` | Auto-detected | Base URL for generating download URLs |
31
+
32
+ ### S3StorageConfig
33
+
34
+ | Property | Type | Default | Description |
35
+ |----------|------|---------|-------------|
36
+ | `type` | `"s3"` | — | **Required.** Must be `"s3"` |
37
+ | `bucket` | `string` | — | **Required.** S3 bucket name |
38
+ | `accessKeyId` | `string` | — | **Required.** AWS access key ID |
39
+ | `secretAccessKey` | `string` | — | **Required.** AWS secret access key |
40
+ | `region` | `string` | `"us-east-1"` | AWS region |
41
+ | `endpoint` | `string` | `undefined` | Custom endpoint URL (required for MinIO, R2, Hetzner, etc.) |
42
+ | `forcePathStyle` | `boolean` | Auto-enabled when `endpoint` is set | Use path-style URLs (required for MinIO) |
43
+ | `maxFileSize` | `number` | `52428800` (50 MB) | Maximum file size in bytes |
44
+ | `allowedMimeTypes` | `string[]` | `undefined` (all allowed) | Restrict uploads to these MIME types |
45
+ | `signedUrlExpiration` | `number` | `3600` | Signed URL expiry in seconds |
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
+
60
+ ### StorageRoutesConfig
61
+
62
+ These options are set internally when mounting routes but are derived from the backend config:
63
+
64
+ | Property | Type | Default | Description |
65
+ |----------|------|---------|-------------|
66
+ | `controller` | `StorageController` | — | **Required.** The storage controller instance |
67
+ | `basePath` | `string` | `"/api/storage"` | Base path for storage routes |
68
+ | `requireAuth` | `boolean` | `true` | Require authentication for write operations |
69
+ | `publicRead` | `boolean` | `false` | Allow unauthenticated read access to stored files |
70
+
71
+ ## Storage Providers
72
+
73
+ ### Local Storage
74
+
75
+ Store files on the local filesystem. Best for development and simple single-server deployments.
76
+
77
+ ```typescript
78
+ import { initializeRebaseBackend } from "@rebasepro/server";
79
+
80
+ const backend = await initializeRebaseBackend({
81
+ server, app,
82
+ bootstrappers: [/* ... */],
83
+ storage: {
84
+ type: "local",
85
+ basePath: "./uploads",
86
+ maxFileSize: 50 * 1024 * 1024, // 50MB (default)
87
+ allowedMimeTypes: ["image/jpeg", "image/png", "application/pdf"],
88
+ baseUrl: "http://localhost:3001/api/storage",
89
+ },
90
+ });
91
+ ```
92
+
93
+ > **WARNING FOR AGENTS:** In production, the backend logs a warning if local storage is used. Local files are lost on container restart. Set `FORCE_LOCAL_STORAGE=true` to suppress the warning, or use S3 for production.
94
+
95
+ Local storage uses a `{basePath}/{bucket}/{path}` directory structure. The default bucket is `"default"`. Every uploaded file gets a sidecar `.metadata.json` file containing:
96
+
97
+ ```json
98
+ {
99
+ "contentType": "image/jpeg",
100
+ "size": 204800,
101
+ "uploadedAt": "2026-01-15T10:30:00.000Z"
102
+ }
103
+ ```
104
+
105
+ Local storage includes **path traversal protection** — any resolved path that escapes the bucket directory throws an error.
106
+
107
+ ### S3-Compatible Storage
108
+
109
+ Works with AWS S3, MinIO, DigitalOcean Spaces, Cloudflare R2, Hetzner Object Storage, Backblaze B2, and any S3-compatible service.
110
+
111
+ ```typescript
112
+ const backend = await initializeRebaseBackend({
113
+ server, app,
114
+ bootstrappers: [/* ... */],
115
+ storage: {
116
+ type: "s3",
117
+ bucket: process.env.S3_BUCKET!,
118
+ region: process.env.S3_REGION,
119
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
120
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
121
+ endpoint: process.env.S3_ENDPOINT, // For MinIO, R2, etc.
122
+ forcePathStyle: true, // Required for MinIO
123
+ signedUrlExpiration: 3600, // URL expiry in seconds
124
+ },
125
+ });
126
+ ```
127
+
128
+ The S3 controller:
129
+ - Auto-enables `forcePathStyle` when a custom `endpoint` is set
130
+ - Maps the logical bucket name `"default"` to the configured S3 bucket
131
+ - Supports `s3://` and `gs://` URL schemes in key parameters for backward compatibility
132
+ - Flattens nested metadata to string values (S3 requirement)
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
+
156
+ ### Multiple Storage Backends
157
+
158
+ Rebase supports multiple storage backends simultaneously via the `StorageRegistry`:
159
+
160
+ ```typescript
161
+ const backend = await initializeRebaseBackend({
162
+ server, app,
163
+ bootstrappers: [/* ... */],
164
+ storage: {
165
+ "(default)": { type: "local", basePath: "./uploads" },
166
+ "media": {
167
+ type: "s3",
168
+ bucket: "my-media-bucket",
169
+ accessKeyId: "...",
170
+ secretAccessKey: "...",
171
+ },
172
+ },
173
+ });
174
+ ```
175
+
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.
177
+
178
+ ### StorageRegistry API
179
+
180
+ The `StorageRegistry` interface:
181
+
182
+ | Method | Signature | Description |
183
+ |--------|-----------|-------------|
184
+ | `register` | `(id: string, controller: StorageController) => void` | Register a controller with an ID |
185
+ | `getDefault` | `() => StorageController` | Get the `(default)` controller (throws if none) |
186
+ | `get` | `(id: string \| undefined \| null) => StorageController \| undefined` | Get by ID, returns `undefined` if not found |
187
+ | `getOrDefault` | `(id: string \| undefined \| null) => StorageController` | Get by ID with fallback to default (throws if neither exists) |
188
+ | `has` | `(id: string) => boolean` | Check if a storage ID exists |
189
+ | `list` | `() => string[]` | List all registered storage IDs |
190
+ | `size` | `() => number` | Number of registered controllers |
191
+
192
+ ### Custom Storage Providers
193
+
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.):
195
+
196
+ ```typescript
197
+ interface StorageController {
198
+ putObject(props: UploadFileProps): Promise<UploadFileResult>;
199
+ getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;
200
+ getObject(key: string, bucket?: string): Promise<File | null>;
201
+ deleteObject(key: string, bucket?: string): Promise<void>;
202
+ listObjects(prefix: string, options?: {
203
+ bucket?: string;
204
+ maxResults?: number;
205
+ pageToken?: string;
206
+ }): Promise<StorageListResult>;
207
+ getType(): string; // e.g. "gcs", "azure"
208
+ }
209
+ ```
210
+
211
+ Pass the instance directly to `storage`:
212
+
213
+ ```typescript
214
+ storage: new MyGCSStorageController({ projectId: "...", bucket: "..." }),
215
+ ```
216
+
217
+ ## Environment Variables
218
+
219
+ The backend validates storage-related environment variables via a Zod schema:
220
+
221
+ | Variable | Type | Default | Description |
222
+ |----------|------|---------|-------------|
223
+ | `STORAGE_TYPE` | `"local" \| "s3" \| "gcs"` | `"local"` | Storage provider type |
224
+ | `STORAGE_PATH` | `string` | `"./uploads"` | Base path for local storage / TUS temp directory |
225
+ | `FORCE_LOCAL_STORAGE` | `"true" \| "false"` | `false` | Suppress the production warning for local storage |
226
+ | `S3_BUCKET` | `string` | — | S3 bucket name |
227
+ | `S3_REGION` | `string` | — | S3 region |
228
+ | `S3_ACCESS_KEY_ID` | `string` | — | S3 access key ID |
229
+ | `S3_SECRET_ACCESS_KEY` | `string` | — | S3 secret access key |
230
+ | `S3_ENDPOINT` | `string` (URL) | — | Custom S3-compatible endpoint |
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 |
235
+
236
+ ```env
237
+ # S3 example
238
+ STORAGE_TYPE=s3
239
+ S3_BUCKET=my-bucket
240
+ S3_REGION=us-east-1
241
+ S3_ACCESS_KEY_ID=AKIA...
242
+ S3_SECRET_ACCESS_KEY=...
243
+ S3_ENDPOINT=https://minio.example.com # Optional: for MinIO, R2, etc.
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
251
+ ```
252
+
253
+ ## Server-Side Storage (custom functions, hooks, jobs)
254
+
255
+ > **🚨 CRITICAL FOR AGENTS — do NOT hand-roll a cloud SDK.** Inside a custom
256
+ > function, collection hook, or cron job, use the configured storage controller
257
+ > at **`rebase.storage`**. **Never** `import "@aws-sdk/client-s3"` or
258
+ > `import "@google-cloud/storage"` (or `new S3Client`, `new Storage()`) directly.
259
+ > Doing so hardcodes one backend, bypasses the `storage` config, duplicates the
260
+ > adapter layer, and breaks the local↔s3↔gcs swap. The backend is a **config
261
+ > choice** (`STORAGE_TYPE`), not something you wire up per feature.
262
+
263
+ `rebase.storage` is the same `StorageController` interface documented above
264
+ (`putObject`, `getObject`, `getSignedUrl`, `deleteObject`, `listObjects`,
265
+ `getType`). It is `undefined` only if no `storage` is configured — guard for that.
266
+
267
+ ```typescript
268
+ import { rebase } from "@rebasepro/server";
269
+
270
+ // Store bytes (e.g. a generated report, an uploaded build context, a thumbnail)
271
+ if (!rebase.storage) throw new Error("Object storage is not configured");
272
+ const file = new File([buffer], "report.pdf", { type: "application/pdf" });
273
+ const { key, bucket, storageUrl } = await rebase.storage.putObject({
274
+ key: "reports/2026/q1.pdf", // your object key
275
+ file, // a web File (Node 20+ has global File)
276
+ });
277
+ // storageUrl is scheme-qualified and matches the CONFIGURED backend:
278
+ // local://<bucket>/<key> | s3://<bucket>/<key> | gs://<bucket>/<key>
279
+
280
+ // Read it back (accepts a key or a full gs://…/s3://… storageUrl)
281
+ const back = await rebase.storage.getObject(key);
282
+ const bytes = back ? Buffer.from(await back.arrayBuffer()) : null;
283
+ ```
284
+
285
+ **Why `storageUrl` matters:** it is the portable, backend-correct URI. Hand it to
286
+ anything that reads object storage directly (a Kaniko build context, an external
287
+ worker, a signed-URL flow) — on GCS it's `gs://…` (read via Workload Identity on
288
+ GKE, no keys), on S3 it's `s3://…`. You never branch on the provider yourself.
289
+
290
+ **Switching backend is env only** — no code change:
291
+ ```env
292
+ STORAGE_TYPE=gcs # was: local (or s3)
293
+ STORAGE_GCS_BUCKET=my-bucket
294
+ # creds via Workload Identity/ADC on GCP, or GOOGLE_APPLICATION_CREDENTIALS
295
+ ```
296
+
297
+ ❌ **Anti-pattern (never do this in app/backend code):**
298
+ ```typescript
299
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; // ❌
300
+ const s3 = new S3Client({ region, endpoint }); // ❌ hardcodes S3
301
+ await s3.send(new PutObjectCommand({ Bucket, Key, Body })); // ❌ bypasses rebase.storage
302
+ ```
303
+ ✅ **Correct:** `await rebase.storage.putObject({ key, file })` (above).
304
+
305
+ *(The only place a cloud SDK is imported directly is inside the storage
306
+ controllers in `@rebasepro/server` themselves — application code always goes
307
+ through `rebase.storage`.)*
308
+
309
+ ## REST API Endpoints
310
+
311
+ 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`.
312
+
313
+ ### Multi-Backend Routing (`storageId`)
314
+
315
+ 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.
316
+
317
+ If `storageId` is omitted, the request is routed to the `(default)` backend.
318
+
319
+ ```
320
+ # Upload to the "media" backend
321
+ POST /api/storage/upload?storageId=media
322
+
323
+ # Download from the "media" backend
324
+ GET /api/storage/file/images/photo.jpg?storageId=media
325
+
326
+ # List files in the "media" backend
327
+ GET /api/storage/list?prefix=images/&storageId=media
328
+ ```
329
+
330
+ ### Standard Endpoints
331
+
332
+ | Method | Endpoint | Auth | Description |
333
+ |--------|----------|------|-------------|
334
+ | `POST` | `/api/storage/upload` | Write | Upload a file (multipart/form-data) |
335
+ | `GET` | `/api/storage/file/*` | Read | Download / serve a file (supports image transforms) |
336
+ | `DELETE` | `/api/storage/file/*` | Write | Delete a file |
337
+ | `GET` | `/api/storage/metadata/*` | Read | Get file metadata |
338
+ | `GET` | `/api/storage/list` | Write | List files in a prefix |
339
+ | `POST` | `/api/storage/folder` | Write | Create a new folder |
340
+
341
+ ### TUS Resumable Upload Endpoints
342
+
343
+ | Method | Endpoint | Auth | Description |
344
+ |--------|----------|------|-------------|
345
+ | `OPTIONS` | `/api/storage/tus` | None | TUS capability discovery |
346
+ | `POST` | `/api/storage/tus` | Write | Create a new resumable upload |
347
+ | `HEAD` | `/api/storage/tus/:id` | Read | Query upload progress |
348
+ | `PATCH` | `/api/storage/tus/:id` | Write | Append data to an upload |
349
+ | `DELETE` | `/api/storage/tus/:id` | Write | Cancel and remove an upload |
350
+
351
+ ### POST /api/storage/upload
352
+
353
+ Upload a file via `multipart/form-data`.
354
+
355
+ **Request body fields:**
356
+
357
+ | Field | Type | Required | Description |
358
+ |-------|------|----------|-------------|
359
+ | `file` | `File` | Yes | The file to upload |
360
+ | `key` | `string` | No | Storage key/path. Falls back to original filename, then `"unnamed"` |
361
+ | `bucket` | `string` | No | Target bucket name |
362
+ | `storageId` | `string` | No | Route to a named backend (alternative to `?storageId` query param) |
363
+ | `metadata_*` | `string` | No | Custom metadata keys prefixed with `metadata_` |
364
+
365
+ **Response** (201):
366
+
367
+ ```json
368
+ {
369
+ "success": true,
370
+ "data": {
371
+ "key": "products/images/photo.jpg",
372
+ "bucket": "default",
373
+ "storageUrl": "local://default/products/images/photo.jpg"
374
+ }
375
+ }
376
+ ```
377
+
378
+ The `storageUrl` format is `local://{bucket}/{key}` for local storage and `s3://{bucket}/{key}` for S3.
379
+
380
+ **Error responses:**
381
+
382
+ | Status | Condition |
383
+ |--------|-----------|
384
+ | 400 | No file provided in request body |
385
+ | 413 | File exceeds `maxFileSize` (body limit middleware) |
386
+
387
+ ### GET /api/storage/file/{path}
388
+
389
+ Download or serve a file. The path can include a bucket prefix (e.g., `/file/default/images/photo.jpg`) or omit it to use the default bucket.
390
+
391
+ For **local storage**, files are served directly from disk with content type read from the `.metadata.json` sidecar file.
392
+
393
+ For **S3 storage**, files are proxied through the backend (not redirected to signed URLs). This avoids mixed-content issues and unreachable internal VPC endpoints.
394
+
395
+ **Image transformation query parameters** can be appended — see [Image Transformation](#image-transformation).
396
+
397
+ **Response headers:**
398
+ - `Content-Type` — from metadata or inferred
399
+ - `Cache-Control: public, max-age=31536000, immutable` (when image transforms are applied)
400
+ - `Cache-Control: public, max-age=3600, immutable` (for S3-proxied files without transforms)
401
+
402
+ ### GET /api/storage/metadata/{path}
403
+
404
+ Get metadata for a file without downloading it.
405
+
406
+ **Response** (200):
407
+
408
+ ```json
409
+ {
410
+ "success": true,
411
+ "data": {
412
+ "bucket": "default",
413
+ "fullPath": "products/images/photo.jpg",
414
+ "name": "photo.jpg",
415
+ "size": 204800,
416
+ "contentType": "image/jpeg",
417
+ "customMetadata": {}
418
+ }
419
+ }
420
+ ```
421
+
422
+ ### GET /api/storage/list
423
+
424
+ List files and folders in a given prefix.
425
+
426
+ **Query parameters:**
427
+
428
+ | Parameter | Type | Default | Description |
429
+ |-----------|------|---------|-------------|
430
+ | `prefix` | `string` | `""` | Path prefix to list (also accepts `path` for backward compat) |
431
+ | `bucket` | `string` | `"default"` (local) | Bucket name |
432
+ | `maxResults` | `number` | `1000` | Maximum number of results |
433
+ | `pageToken` | `string` | — | Pagination token from previous response |
434
+
435
+ **Response** (200):
436
+
437
+ ```json
438
+ {
439
+ "success": true,
440
+ "data": {
441
+ "items": [
442
+ { "bucket": "default", "fullPath": "images/photo.jpg", "name": "photo.jpg" }
443
+ ],
444
+ "prefixes": [
445
+ { "bucket": "default", "fullPath": "images/thumbnails", "name": "thumbnails" }
446
+ ],
447
+ "nextPageToken": "25"
448
+ }
449
+ }
450
+ ```
451
+
452
+ - `items` — files in the prefix
453
+ - `prefixes` — subdirectories/folders in the prefix
454
+ - `nextPageToken` — pass to the next request for pagination (only present if more results exist)
455
+
456
+ For S3, listing uses the `Delimiter: "/"` for folder-like behavior via `ListObjectsV2Command`.
457
+
458
+ ### POST /api/storage/folder
459
+
460
+ Create a new folder.
461
+
462
+ **Request body:**
463
+
464
+ ```json
465
+ {
466
+ "path": "products/images/thumbnails",
467
+ "bucket": "default"
468
+ }
469
+ ```
470
+
471
+ For **local storage**, creates the directory on disk. For **S3**, creates a zero-byte marker object with a trailing `/` and content type `application/x-directory`.
472
+
473
+ **Response** (201):
474
+
475
+ ```json
476
+ { "success": true, "message": "Folder created" }
477
+ ```
478
+
479
+ ### DELETE /api/storage/file/{path}
480
+
481
+ Delete a file. For local storage, also deletes the `.metadata.json` sidecar file. Directories can only be deleted if empty (ENOTEMPTY errors are silently ignored).
482
+
483
+ **Response** (200):
484
+
485
+ ```json
486
+ { "success": true, "message": "File deleted" }
487
+ ```
488
+
489
+ ## TUS Resumable Uploads
490
+
491
+ Rebase implements [TUS v1.0.0](https://tus.io/protocols/resumable-upload) with the **Creation** and **Termination** extensions. This allows reliable upload of large files (up to 5 GB) with automatic resume on network failure.
492
+
493
+ ### TUS Configuration
494
+
495
+ | Setting | Value |
496
+ |---------|-------|
497
+ | TUS version | `1.0.0` |
498
+ | Extensions | `creation`, `termination` |
499
+ | Max upload size | 5 GB (`5 * 1024 * 1024 * 1024` bytes) |
500
+ | Stale upload expiry | 24 hours |
501
+ | Cleanup interval | Every 60 seconds |
502
+ | Temp directory | `{storageBasePath}/.tus-uploads` |
503
+
504
+ TUS uploads are stored in a temporary directory and automatically moved to the final storage backend (via `putObject`) when the upload completes.
505
+
506
+ ### TUS Protocol Flow
507
+
508
+ ```
509
+ 1. OPTIONS /api/storage/tus → Discover capabilities
510
+ 2. POST /api/storage/tus → Create upload, get Location header
511
+ 3. PATCH /api/storage/tus/:id → Send chunks (repeat until done)
512
+ 4. (auto) Upload finalized → moved to storage controller
513
+ ```
514
+
515
+ ### OPTIONS /api/storage/tus
516
+
517
+ Returns TUS server capabilities.
518
+
519
+ **Response headers:**
520
+
521
+ ```
522
+ Tus-Resumable: 1.0.0
523
+ Tus-Version: 1.0.0
524
+ Tus-Extension: creation,termination
525
+ Tus-Max-Size: 5368709120
526
+ ```
527
+
528
+ ### POST /api/storage/tus (Create)
529
+
530
+ Create a new resumable upload.
531
+
532
+ **Required request headers:**
533
+
534
+ | Header | Description |
535
+ |--------|-------------|
536
+ | `Upload-Length` | Total file size in bytes (must be > 0 and ≤ 5 GB) |
537
+ | `Upload-Metadata` | TUS metadata as `key base64value,key2 base64value2` |
538
+
539
+ **Supported metadata keys:**
540
+
541
+ | Key | Description |
542
+ |-----|-------------|
543
+ | `filename` | Original file name (used as storage key if no `key` provided) |
544
+ | `key` | Explicit storage key/path |
545
+ | `bucket` | Target bucket |
546
+ | `contentType` | File MIME type |
547
+ | `filetype` | Alternative MIME type key (fallback) |
548
+
549
+ **Response** (201):
550
+
551
+ ```
552
+ Location: http://localhost:3001/api/storage/tus/550e8400-e29b-41d4-a716-446655440000
553
+ Tus-Resumable: 1.0.0
554
+ Upload-Offset: 0
555
+ ```
556
+
557
+ ### HEAD /api/storage/tus/:id (Progress)
558
+
559
+ Query the current upload progress.
560
+
561
+ **Response** (200):
562
+
563
+ ```
564
+ Tus-Resumable: 1.0.0
565
+ Upload-Offset: 1048576
566
+ Upload-Length: 10485760
567
+ Cache-Control: no-store
568
+ ```
569
+
570
+ ### PATCH /api/storage/tus/:id (Append)
571
+
572
+ Append data to an in-progress upload.
573
+
574
+ **Required request headers:**
575
+
576
+ | Header | Value |
577
+ |--------|-------|
578
+ | `Content-Type` | `application/offset+octet-stream` |
579
+ | `Upload-Offset` | Must match the server's current offset |
580
+
581
+ **Error responses:**
582
+
583
+ | Status | Condition |
584
+ |--------|-----------|
585
+ | 400 | Missing `Upload-Offset` header, or upload already completed |
586
+ | 409 | Offset mismatch (client offset ≠ server offset) |
587
+ | 413 | Chunk would exceed declared `Upload-Length` |
588
+ | 415 | Wrong `Content-Type` (must be `application/offset+octet-stream`) |
589
+
590
+ When `Upload-Offset` reaches `Upload-Length`, the upload is **automatically finalized**: the temp file is read, a `File` object is created with the metadata-derived MIME type, `putObject` is called on the storage controller, and the temp file is cleaned up.
591
+
592
+ ### DELETE /api/storage/tus/:id (Terminate)
593
+
594
+ Cancel and remove an in-progress upload. Deletes the temp file and removes the upload from the in-memory registry.
595
+
596
+ ### TUS Client Example
597
+
598
+ ```typescript
599
+ // Using tus-js-client (npm install tus-js-client)
600
+ import * as tus from "tus-js-client";
601
+
602
+ const file = document.querySelector<HTMLInputElement>("#fileInput")!.files![0];
603
+
604
+ const upload = new tus.Upload(file, {
605
+ endpoint: "http://localhost:3001/api/storage/tus",
606
+ retryDelays: [0, 1000, 3000, 5000],
607
+ metadata: {
608
+ filename: file.name,
609
+ filetype: file.type,
610
+ key: `uploads/${file.name}`,
611
+ bucket: "default",
612
+ },
613
+ headers: {
614
+ Authorization: `Bearer ${accessToken}`,
615
+ },
616
+ onError: (error) => console.error("Upload failed:", error),
617
+ onProgress: (bytesUploaded, bytesTotal) => {
618
+ const pct = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
619
+ console.log(`${pct}%`);
620
+ },
621
+ onSuccess: () => console.log("Upload complete:", upload.url),
622
+ });
623
+
624
+ upload.start();
625
+ ```
626
+
627
+ ## Image Transformation
628
+
629
+ Rebase provides on-the-fly image resize, crop, format conversion, and quality adjustment. Transformations are applied by adding query parameters to the `GET /api/storage/file/*` endpoint.
630
+
631
+ > **IMPORTANT FOR AGENTS:** Image transformation requires the `sharp` npm package as an **optional peer dependency**. Install it: `pnpm add sharp`. If `sharp` is not installed, requesting a transform will throw an error.
632
+
633
+ ### Transform Query Parameters
634
+
635
+ | Parameter | Type | Range | Default | Description |
636
+ |-----------|------|-------|---------|-------------|
637
+ | `width` | `number` | 1–4096 | — | Target width in pixels |
638
+ | `height` | `number` | 1–4096 | — | Target height in pixels |
639
+ | `quality` | `number` | 1–100 | `80` | Output quality |
640
+ | `format` | `string` | `webp`, `avif`, `jpeg`, `png` | `webp` | Output format |
641
+ | `fit` | `string` | `cover`, `contain`, `fill`, `inside`, `outside` | `cover` | Resize fit mode |
642
+
643
+ All dimensions are capped at **4096 px** to prevent abuse. `withoutEnlargement` is enabled — images are never upscaled.
644
+
645
+ ### Transform Examples
646
+
647
+ ```
648
+ # Resize to 300px wide, auto height, WebP format
649
+ GET /api/storage/file/default/images/photo.jpg?width=300
650
+
651
+ # Resize to 800x600, JPEG at 90% quality
652
+ GET /api/storage/file/default/images/photo.jpg?width=800&height=600&format=jpeg&quality=90
653
+
654
+ # Convert to AVIF with contain fit
655
+ GET /api/storage/file/default/images/photo.jpg?format=avif&fit=contain&width=500&height=500
656
+ ```
657
+
658
+ ### Transformable Image Types
659
+
660
+ Only raster images are transformable. SVG and GIF are **excluded**:
661
+
662
+ ```
663
+ ✅ image/jpeg, image/png, image/webp, image/bmp, image/tiff
664
+ ❌ image/svg+xml, image/gif
665
+ ```
666
+
667
+ ### Transform Cache
668
+
669
+ Transformed images are cached in an **LRU in-memory cache** to avoid redundant processing:
670
+
671
+ | Setting | Value |
672
+ |---------|-------|
673
+ | Max entries | 500 |
674
+ | TTL | 1 hour (3,600,000 ms) |
675
+ | Cache key | `{filePath}::{JSON.stringify(options)}` |
676
+ | Eviction policy | LRU (oldest entry evicted when at capacity) |
677
+
678
+ Transformed responses include `Cache-Control: public, max-age=31536000, immutable`.
679
+
680
+ ## Client SDK Methods
681
+
682
+ The `@rebasepro/client` package provides a `StorageSource` interface via `createStorage(transport)`. These methods are available on `client.storage` (or through the React/Studio hooks).
683
+
684
+ All SDK methods accept an optional `storageId` parameter (passed as a query parameter) to route the request to a specific named backend:
685
+
686
+ ```typescript
687
+ // Upload to a specific backend
688
+ const result = await client.storage.putObject({
689
+ file: myFile,
690
+ key: "products/images/photo.jpg",
691
+ storageId: "media", // routes to the "media" backend
692
+ });
693
+
694
+ // Get signed URL from a specific backend
695
+ const config = await client.storage.getSignedUrl(
696
+ "products/images/photo.jpg",
697
+ "default", // bucket
698
+ { storageId: "media" }
699
+ );
700
+ ```
701
+
702
+ ### putObject
703
+
704
+ Upload a file.
705
+
706
+ ```typescript
707
+ const result = await client.storage.putObject({
708
+ file: myFile, // File object
709
+ key: "products/images/photo.jpg", // Storage key/path
710
+ bucket: "default", // Optional bucket
711
+ metadata: { category: "product" }, // Optional custom metadata
712
+ });
713
+ // result: { key: "products/images/photo.jpg", bucket: "default", storageUrl: "local://..." }
714
+ ```
715
+
716
+ The SDK sends a `multipart/form-data` request to `POST /api/storage/upload`. Custom metadata keys are prefixed with `metadata_` in the form data.
717
+
718
+ ### getSignedUrl
719
+
720
+ Get a download URL and metadata for a file. Results are cached in-memory on the client.
721
+
722
+ ```typescript
723
+ const config = await client.storage.getSignedUrl(
724
+ "products/images/photo.jpg", // key or storageUrl (local:// or s3://)
725
+ "default" // optional bucket
726
+ );
727
+
728
+ if (config.fileNotFound) {
729
+ console.log("File does not exist");
730
+ } else {
731
+ console.log(config.url); // Private: short-lived scoped token appended. Public: clean permanent URL.
732
+ console.log(config.metadata); // { bucket, fullPath, name, size, contentType, customMetadata }
733
+ }
734
+ ```
735
+
736
+ **URL model — never contains the access token:**
737
+
738
+ - **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.
739
+ - **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.
740
+
741
+ Protocol prefixes (`local://`, `s3://`) are automatically stripped from the key.
742
+
743
+ ### getObject
744
+
745
+ Download a file as a `File` object.
746
+
747
+ ```typescript
748
+ const file = await client.storage.getObject(
749
+ "products/images/photo.jpg", // key or storageUrl
750
+ "default" // optional bucket
751
+ );
752
+
753
+ if (file) {
754
+ console.log(file.name); // "photo.jpg"
755
+ console.log(file.type); // "image/jpeg"
756
+ const blob = new Blob([file]);
757
+ // use blob for display, further processing, etc.
758
+ }
759
+ ```
760
+
761
+ > **IMPORTANT FOR AGENTS:** `getObject` uses raw `fetch` (not the JSON transport) because the response is a binary blob, not JSON.
762
+
763
+ ### deleteObject
764
+
765
+ Delete a file. Silently ignores 404 errors (file already deleted).
766
+
767
+ ```typescript
768
+ await client.storage.deleteObject(
769
+ "products/images/photo.jpg", // key or storageUrl
770
+ "default" // optional bucket
771
+ );
772
+ ```
773
+
774
+ ### listObjects
775
+
776
+ List files and folders in a prefix with pagination.
777
+
778
+ ```typescript
779
+ const result = await client.storage.listObjects("products/images", {
780
+ bucket: "default",
781
+ maxResults: 50,
782
+ pageToken: undefined, // from previous result.nextPageToken
783
+ });
784
+
785
+ console.log(result.items); // files: StorageReference[]
786
+ console.log(result.prefixes); // folders: StorageReference[]
787
+ console.log(result.nextPageToken); // for next page, or undefined
788
+ ```
789
+
790
+ ## Collection File Upload Properties
791
+
792
+ Define file upload fields in your collections using the `storage` option on string properties:
793
+
794
+ ```typescript
795
+ import { PostgresCollectionConfig } from "@rebasepro/types";
796
+
797
+ const productsCollection: PostgresCollectionConfig = {
798
+ name: "Products",
799
+ table: "products",
800
+ properties: {
801
+ image: {
802
+ name: "Product Image",
803
+ type: "string",
804
+ storage: {
805
+ storageSource: "external-source",
806
+ storagePath: "products/{entityId}",
807
+ acceptedFiles: ["image/*"],
808
+ maxSize: 5 * 1024 * 1024, // 5MB
809
+ }
810
+ },
811
+ documents: {
812
+ name: "Documents",
813
+ type: "array",
814
+ of: {
815
+ type: "string",
816
+ storage: {
817
+ storageSource: "media", // routes to the "media" backend
818
+ storagePath: "products/documents",
819
+ acceptedFiles: ["application/pdf", "application/msword"],
820
+ }
821
+ }
822
+ }
823
+ }
824
+ };
825
+ ```
826
+
827
+ 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.
828
+
829
+ ### StorageConfig Properties
830
+
831
+ | Property | Type | Description |
832
+ |----------|------|-------------|
833
+ | `storageSource` | `string` | Named backend to route uploads to (optional, defaults to `(default)`) |
834
+ | `storagePath` | `string` | Path template for uploaded files. Supports `{entityId}` placeholder |
835
+ | `acceptedFiles` | `string[]` | Accepted MIME types or globs (e.g., `["image/*"]`) |
836
+ | `maxSize` | `number` | Maximum file size in bytes |
837
+ | `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). |
838
+
839
+ ## Frontend Storage Sources
840
+
841
+ 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.
842
+
843
+ ### Registering Storage Sources
844
+
845
+ ```tsx
846
+ import type { RebaseStorageSource } from "@rebasepro/app";
847
+ import { myExternalStorageSource } from "./my-external-storage";
848
+
849
+ <Rebase
850
+ client={rebaseClient}
851
+ storageSources={[
852
+ {
853
+ key: "external",
854
+ engine: "external",
855
+ transport: "direct",
856
+ source: myExternalStorageSource,
857
+ },
858
+ {
859
+ key: "media",
860
+ engine: "s3",
861
+ transport: "server", // proxied through the Rebase backend
862
+ label: "Media Storage",
863
+ },
864
+ ]}
865
+ >
866
+ {children}
867
+ </Rebase>
868
+ ```
869
+
870
+ ### StorageSourceDefinition
871
+
872
+ | Property | Type | Description |
873
+ |----------|------|-------------|
874
+ | `key` | `string` | **Required.** Unique identifier for this storage source |
875
+ | `engine` | `string` | **Required.** Storage engine type (e.g., `"firebase"`, `"s3"`, `"gcs"`, `"local"`) |
876
+ | `transport` | `"server" \| "direct"` | **Required.** `"server"` routes through the backend; `"direct"` talks to the provider from the client |
877
+ | `source` | `StorageSource` | The client-side `StorageSource` implementation (required when `transport` is `"direct"`) |
878
+ | `label` | `string` | Optional human-readable label for the UI |
879
+
880
+ ### StorageSourcesContext
881
+
882
+ Registered storage sources are exposed to the component tree via `StorageSourcesContext`, mirroring the `DataSourcesContext` pattern:
883
+
884
+ ```typescript
885
+ import { useStorageSources } from "@rebasepro/app";
886
+
887
+ function MyUploadComponent() {
888
+ const storageSources = useStorageSources();
889
+
890
+ // Get a specific source
891
+ const externalSource = storageSources.find(s => s.key === "external");
892
+
893
+ // Use it for direct uploads
894
+ if (externalSource?.source) {
895
+ await externalSource.source.putObject({ file, key: "photos/image.jpg" });
896
+ }
897
+ }
898
+ ```
899
+
900
+ ### Direct vs Server Transport
901
+
902
+ | Transport | Description | Use Case |
903
+ |-----------|-------------|----------|
904
+ | `"server"` | File operations are proxied through the Rebase backend REST API | S3, GCS, or local backends managed by the server |
905
+ | `"direct"` | File operations go directly from the client to the storage provider | External cloud storage, or any provider with client-side SDKs |
906
+
907
+ > **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`.
908
+
909
+ ## Storage Browser (Studio)
910
+
911
+ The `@rebasepro/studio` package includes a built-in `StorageView` component:
912
+ - Browse uploaded files and folders with a tree sidebar
913
+ - Drag-and-drop file uploads
914
+ - Image, video, and audio previews with metadata
915
+ - File search and filtering
916
+ - Grid and list view modes
917
+
918
+ ## Validation and Error Handling
919
+
920
+ ### File Size Validation
921
+
922
+ File size is validated at two levels:
923
+
924
+ 1. **Hono body limit middleware** — returns `413` with `PAYLOAD_TOO_LARGE` before the file reaches the controller. The limit is derived from `maxFileSize` in the storage config (default 50 MB).
925
+ 2. **Controller-level validation** — the controller's `validateFile()` checks `file.size` against `maxFileSize` and throws an `Error` if exceeded.
926
+
927
+ ### MIME Type Validation
928
+
929
+ When `allowedMimeTypes` is set in the storage config, uploads with disallowed MIME types are rejected with an error:
930
+
931
+ ```
932
+ File type application/zip is not allowed. Allowed types: image/jpeg, image/png
933
+ ```
934
+
935
+ ### Common MIME Type Constants
936
+
937
+ The storage module exports two convenience arrays:
938
+
939
+ ```typescript
940
+ import { IMAGE_MIME_TYPES, DOCUMENT_MIME_TYPES } from "@rebasepro/server";
941
+
942
+ // IMAGE_MIME_TYPES:
943
+ // ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml", "image/bmp", "image/tiff"]
944
+
945
+ // DOCUMENT_MIME_TYPES:
946
+ // ["application/pdf", "application/msword",
947
+ // "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
948
+ // "application/vnd.ms-excel",
949
+ // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
950
+ // "application/vnd.ms-powerpoint",
951
+ // "application/vnd.openxmlformats-officedocument.presentationml.presentation",
952
+ // "text/plain", "text/csv"]
953
+ ```
954
+
955
+ ### TUS Error Codes
956
+
957
+ | HTTP Status | Condition |
958
+ |-------------|-----------|
959
+ | 400 | Missing/invalid `Upload-Length` header, missing `Upload-Offset`, upload already completed |
960
+ | 404 | Upload ID not found |
961
+ | 409 | `Upload-Offset` mismatch (client offset ≠ server offset) |
962
+ | 413 | `Upload-Length` exceeds 5 GB max, or chunk exceeds declared length |
963
+ | 415 | `Content-Type` is not `application/offset+octet-stream` |
964
+
965
+ ## Metadata Sidecar Files (Local Storage)
966
+
967
+ For local storage, every uploaded file has a companion `.metadata.json` file saved alongside it. This sidecar file stores:
968
+
969
+ ```json
970
+ {
971
+ "contentType": "image/jpeg",
972
+ "size": 204800,
973
+ "uploadedAt": "2026-01-15T10:30:00.000Z",
974
+ "category": "product"
975
+ }
976
+ ```
977
+
978
+ - `contentType` and `size` are always present
979
+ - Custom metadata from the upload request is merged in
980
+ - The content type from this file is used when serving files via `GET /file/*`
981
+ - When a file is deleted, its `.metadata.json` is also deleted
982
+ - Metadata files are skipped when listing directory contents (filtered out by name pattern)
983
+
984
+ ## References
985
+
986
+ - **Documentation:** [rebase.pro/docs](https://rebase.pro/docs)
987
+ - **GitHub:** [github.com/rebasepro/rebase](https://github.com/rebasepro/rebase)
988
+ - **TUS Protocol:** [tus.io/protocols/resumable-upload](https://tus.io/protocols/resumable-upload)
989
+ - **Sharp (image transforms):** [sharp.pixelplumbing.com](https://sharp.pixelplumbing.com)