@rebasepro/cli 0.5.0 → 0.6.1

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 (61) hide show
  1. package/dist/commands/init.d.ts +13 -0
  2. package/dist/commands/skills.d.ts +1 -0
  3. package/dist/index.es.js +1631 -1308
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/utils/package-manager.d.ts +2 -0
  6. package/package.json +17 -16
  7. package/skills/rebase-admin/SKILL.md +710 -0
  8. package/skills/rebase-api/SKILL.md +662 -0
  9. package/skills/rebase-api/references/.gitkeep +3 -0
  10. package/skills/rebase-auth/SKILL.md +1143 -0
  11. package/skills/rebase-auth/references/.gitkeep +3 -0
  12. package/skills/rebase-backend-postgres/SKILL.md +633 -0
  13. package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
  14. package/skills/rebase-basics/SKILL.md +749 -0
  15. package/skills/rebase-basics/references/.gitkeep +3 -0
  16. package/skills/rebase-collections/SKILL.md +1328 -0
  17. package/skills/rebase-collections/references/.gitkeep +3 -0
  18. package/skills/rebase-cron-jobs/SKILL.md +699 -0
  19. package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
  20. package/skills/rebase-custom-functions/SKILL.md +233 -0
  21. package/skills/rebase-deployment/SKILL.md +885 -0
  22. package/skills/rebase-deployment/references/.gitkeep +3 -0
  23. package/skills/rebase-design-language/SKILL.md +692 -0
  24. package/skills/rebase-email/SKILL.md +701 -0
  25. package/skills/rebase-email/references/.gitkeep +1 -0
  26. package/skills/rebase-entity-history/SKILL.md +485 -0
  27. package/skills/rebase-entity-history/references/.gitkeep +1 -0
  28. package/skills/rebase-local-env-setup/SKILL.md +189 -0
  29. package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
  30. package/skills/rebase-realtime/SKILL.md +755 -0
  31. package/skills/rebase-realtime/references/.gitkeep +3 -0
  32. package/skills/rebase-sdk/SKILL.md +594 -0
  33. package/skills/rebase-sdk/references/.gitkeep +0 -0
  34. package/skills/rebase-storage/SKILL.md +765 -0
  35. package/skills/rebase-storage/references/.gitkeep +3 -0
  36. package/skills/rebase-studio/SKILL.md +746 -0
  37. package/skills/rebase-studio/references/.gitkeep +3 -0
  38. package/skills/rebase-ui-components/SKILL.md +1488 -0
  39. package/skills/rebase-ui-components/references/.gitkeep +3 -0
  40. package/skills/rebase-webhooks/SKILL.md +623 -0
  41. package/skills/rebase-webhooks/references/.gitkeep +1 -0
  42. package/templates/template/AGENTS.md +2 -0
  43. package/templates/template/CLAUDE.md +2 -0
  44. package/templates/template/ai-instructions.md +6 -3
  45. package/templates/template/backend/package.json +1 -1
  46. package/templates/template/backend/src/env.ts +1 -1
  47. package/templates/template/backend/src/index.ts +9 -6
  48. package/templates/template/config/collections/presets/ecommerce/orders.ts +15 -5
  49. package/templates/template/config/collections/presets/ecommerce/products.ts +9 -3
  50. package/templates/template/config/collections/users.ts +7 -10
  51. package/templates/template/frontend/package.json +2 -2
  52. package/templates/template/frontend/src/App.tsx +1 -7
  53. package/templates/template/frontend/vite.config.ts +0 -1
  54. package/templates/template/package.json +1 -0
  55. package/dist/commands/cli.test.d.ts +0 -1
  56. package/dist/commands/dev.test.d.ts +0 -1
  57. package/dist/commands/init.test.d.ts +0 -1
  58. package/dist/index.cjs +0 -1575
  59. package/dist/index.cjs.map +0 -1
  60. package/dist/utils/package-manager.test.d.ts +0 -1
  61. package/dist/utils/project.test.d.ts +0 -1
@@ -0,0 +1,765 @@
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 or local file storage, handle file uploads, TUS resumable uploads, image transformations, or integrate the media manager.
4
+ ---
5
+
6
+ # Rebase Storage
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.
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', ... }` — 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
+ ### StorageRoutesConfig
48
+
49
+ These options are set internally when mounting routes but are derived from the backend config:
50
+
51
+ | Property | Type | Default | Description |
52
+ |----------|------|---------|-------------|
53
+ | `controller` | `StorageController` | — | **Required.** The storage controller instance |
54
+ | `basePath` | `string` | `"/api/storage"` | Base path for storage routes |
55
+ | `requireAuth` | `boolean` | `true` | Require authentication for write operations |
56
+ | `publicRead` | `boolean` | `false` | Allow unauthenticated read access to stored files |
57
+
58
+ ## Storage Providers
59
+
60
+ ### Local Storage
61
+
62
+ Store files on the local filesystem. Best for development and simple single-server deployments.
63
+
64
+ ```typescript
65
+ import { initializeRebaseBackend } from "@rebasepro/server-core";
66
+
67
+ const backend = await initializeRebaseBackend({
68
+ server, app,
69
+ bootstrappers: [/* ... */],
70
+ storage: {
71
+ type: "local",
72
+ basePath: "./uploads",
73
+ maxFileSize: 50 * 1024 * 1024, // 50MB (default)
74
+ allowedMimeTypes: ["image/jpeg", "image/png", "application/pdf"],
75
+ baseUrl: "http://localhost:3001/api/storage",
76
+ },
77
+ });
78
+ ```
79
+
80
+ > **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.
81
+
82
+ Local storage uses a `{basePath}/{bucket}/{path}` directory structure. The default bucket is `"default"`. Every uploaded file gets a sidecar `.metadata.json` file containing:
83
+
84
+ ```json
85
+ {
86
+ "contentType": "image/jpeg",
87
+ "size": 204800,
88
+ "uploadedAt": "2026-01-15T10:30:00.000Z"
89
+ }
90
+ ```
91
+
92
+ Local storage includes **path traversal protection** — any resolved path that escapes the bucket directory throws an error.
93
+
94
+ ### S3-Compatible Storage
95
+
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.
97
+
98
+ ```typescript
99
+ const backend = await initializeRebaseBackend({
100
+ server, app,
101
+ bootstrappers: [/* ... */],
102
+ storage: {
103
+ type: "s3",
104
+ bucket: process.env.S3_BUCKET!,
105
+ region: process.env.S3_REGION,
106
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
107
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
108
+ endpoint: process.env.S3_ENDPOINT, // For MinIO, R2, etc.
109
+ forcePathStyle: true, // Required for MinIO
110
+ signedUrlExpiration: 3600, // URL expiry in seconds
111
+ },
112
+ });
113
+ ```
114
+
115
+ The S3 controller:
116
+ - Auto-enables `forcePathStyle` when a custom `endpoint` is set
117
+ - Maps the logical bucket name `"default"` to the configured S3 bucket
118
+ - Supports `s3://` and `gs://` URL schemes in key parameters for backward compatibility
119
+ - Flattens nested metadata to string values (S3 requirement)
120
+
121
+ ### Multiple Storage Backends
122
+
123
+ Rebase supports multiple storage backends simultaneously via the `StorageRegistry`:
124
+
125
+ ```typescript
126
+ const backend = await initializeRebaseBackend({
127
+ server, app,
128
+ bootstrappers: [/* ... */],
129
+ storage: {
130
+ "(default)": { type: "local", basePath: "./uploads" },
131
+ "media": {
132
+ type: "s3",
133
+ bucket: "my-media-bucket",
134
+ accessKeyId: "...",
135
+ secretAccessKey: "...",
136
+ },
137
+ },
138
+ });
139
+ ```
140
+
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.
142
+
143
+ ### StorageRegistry API
144
+
145
+ The `StorageRegistry` interface:
146
+
147
+ | Method | Signature | Description |
148
+ |--------|-----------|-------------|
149
+ | `register` | `(id: string, controller: StorageController) => void` | Register a controller with an ID |
150
+ | `getDefault` | `() => StorageController` | Get the `(default)` controller (throws if none) |
151
+ | `get` | `(id: string \| undefined \| null) => StorageController \| undefined` | Get by ID, returns `undefined` if not found |
152
+ | `getOrDefault` | `(id: string \| undefined \| null) => StorageController` | Get by ID with fallback to default (throws if neither exists) |
153
+ | `has` | `(id: string) => boolean` | Check if a storage ID exists |
154
+ | `list` | `() => string[]` | List all registered storage IDs |
155
+ | `size` | `() => number` | Number of registered controllers |
156
+
157
+ ### Custom Storage Providers
158
+
159
+ Implement the `StorageController` interface for unsupported providers (Azure Blob, native GCS SDK, etc.):
160
+
161
+ ```typescript
162
+ interface StorageController {
163
+ putObject(props: UploadFileProps): Promise<UploadFileResult>;
164
+ getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;
165
+ getObject(key: string, bucket?: string): Promise<File | null>;
166
+ deleteObject(key: string, bucket?: string): Promise<void>;
167
+ listObjects(prefix: string, options?: {
168
+ bucket?: string;
169
+ maxResults?: number;
170
+ pageToken?: string;
171
+ }): Promise<StorageListResult>;
172
+ getType(): string; // e.g. "gcs", "azure"
173
+ }
174
+ ```
175
+
176
+ Pass the instance directly to `storage`:
177
+
178
+ ```typescript
179
+ storage: new MyGCSStorageController({ projectId: "...", bucket: "..." }),
180
+ ```
181
+
182
+ ## Environment Variables
183
+
184
+ The backend validates storage-related environment variables via a Zod schema:
185
+
186
+ | Variable | Type | Default | Description |
187
+ |----------|------|---------|-------------|
188
+ | `STORAGE_TYPE` | `"local" \| "s3"` | `"local"` | Storage provider type |
189
+ | `STORAGE_PATH` | `string` | `"./uploads"` | Base path for local storage / TUS temp directory |
190
+ | `FORCE_LOCAL_STORAGE` | `"true" \| "false"` | `false` | Suppress the production warning for local storage |
191
+ | `S3_BUCKET` | `string` | — | S3 bucket name |
192
+ | `S3_REGION` | `string` | — | S3 region |
193
+ | `S3_ACCESS_KEY_ID` | `string` | — | S3 access key ID |
194
+ | `S3_SECRET_ACCESS_KEY` | `string` | — | S3 secret access key |
195
+ | `S3_ENDPOINT` | `string` (URL) | — | Custom S3-compatible endpoint |
196
+ | `S3_FORCE_PATH_STYLE` | `"true" \| "false"` | — | Enable path-style URLs |
197
+
198
+ ```env
199
+ STORAGE_TYPE=s3
200
+ S3_BUCKET=my-bucket
201
+ S3_REGION=us-east-1
202
+ S3_ACCESS_KEY_ID=AKIA...
203
+ S3_SECRET_ACCESS_KEY=...
204
+ S3_ENDPOINT=https://minio.example.com # Optional: for MinIO, R2, etc.
205
+ S3_FORCE_PATH_STYLE=true # Optional: for MinIO
206
+ ```
207
+
208
+ ## REST API Endpoints
209
+
210
+ 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
+
212
+ ### Standard Endpoints
213
+
214
+ | Method | Endpoint | Auth | Description |
215
+ |--------|----------|------|-------------|
216
+ | `POST` | `/api/storage/upload` | Write | Upload a file (multipart/form-data) |
217
+ | `GET` | `/api/storage/file/*` | Read | Download / serve a file (supports image transforms) |
218
+ | `DELETE` | `/api/storage/file/*` | Write | Delete a file |
219
+ | `GET` | `/api/storage/metadata/*` | Read | Get file metadata |
220
+ | `GET` | `/api/storage/list` | Write | List files in a prefix |
221
+ | `POST` | `/api/storage/folder` | Write | Create a new folder |
222
+
223
+ ### TUS Resumable Upload Endpoints
224
+
225
+ | Method | Endpoint | Auth | Description |
226
+ |--------|----------|------|-------------|
227
+ | `OPTIONS` | `/api/storage/tus` | None | TUS capability discovery |
228
+ | `POST` | `/api/storage/tus` | Write | Create a new resumable upload |
229
+ | `HEAD` | `/api/storage/tus/:id` | Read | Query upload progress |
230
+ | `PATCH` | `/api/storage/tus/:id` | Write | Append data to an upload |
231
+ | `DELETE` | `/api/storage/tus/:id` | Write | Cancel and remove an upload |
232
+
233
+ ### POST /api/storage/upload
234
+
235
+ Upload a file via `multipart/form-data`.
236
+
237
+ **Request body fields:**
238
+
239
+ | Field | Type | Required | Description |
240
+ |-------|------|----------|-------------|
241
+ | `file` | `File` | Yes | The file to upload |
242
+ | `key` | `string` | No | Storage key/path. Falls back to original filename, then `"unnamed"` |
243
+ | `bucket` | `string` | No | Target bucket name |
244
+ | `metadata_*` | `string` | No | Custom metadata keys prefixed with `metadata_` |
245
+
246
+ **Response** (201):
247
+
248
+ ```json
249
+ {
250
+ "success": true,
251
+ "data": {
252
+ "key": "products/images/photo.jpg",
253
+ "bucket": "default",
254
+ "storageUrl": "local://default/products/images/photo.jpg"
255
+ }
256
+ }
257
+ ```
258
+
259
+ The `storageUrl` format is `local://{bucket}/{key}` for local storage and `s3://{bucket}/{key}` for S3.
260
+
261
+ **Error responses:**
262
+
263
+ | Status | Condition |
264
+ |--------|-----------|
265
+ | 400 | No file provided in request body |
266
+ | 413 | File exceeds `maxFileSize` (body limit middleware) |
267
+
268
+ ### GET /api/storage/file/{path}
269
+
270
+ 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.
271
+
272
+ For **local storage**, files are served directly from disk with content type read from the `.metadata.json` sidecar file.
273
+
274
+ For **S3 storage**, files are proxied through the backend (not redirected to signed URLs). This avoids mixed-content issues and unreachable internal VPC endpoints.
275
+
276
+ **Image transformation query parameters** can be appended — see [Image Transformation](#image-transformation).
277
+
278
+ **Response headers:**
279
+ - `Content-Type` — from metadata or inferred
280
+ - `Cache-Control: public, max-age=31536000, immutable` (when image transforms are applied)
281
+ - `Cache-Control: public, max-age=3600, immutable` (for S3-proxied files without transforms)
282
+
283
+ ### GET /api/storage/metadata/{path}
284
+
285
+ Get metadata for a file without downloading it.
286
+
287
+ **Response** (200):
288
+
289
+ ```json
290
+ {
291
+ "success": true,
292
+ "data": {
293
+ "bucket": "default",
294
+ "fullPath": "products/images/photo.jpg",
295
+ "name": "photo.jpg",
296
+ "size": 204800,
297
+ "contentType": "image/jpeg",
298
+ "customMetadata": {}
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### GET /api/storage/list
304
+
305
+ List files and folders in a given prefix.
306
+
307
+ **Query parameters:**
308
+
309
+ | Parameter | Type | Default | Description |
310
+ |-----------|------|---------|-------------|
311
+ | `prefix` | `string` | `""` | Path prefix to list (also accepts `path` for backward compat) |
312
+ | `bucket` | `string` | `"default"` (local) | Bucket name |
313
+ | `maxResults` | `number` | `1000` | Maximum number of results |
314
+ | `pageToken` | `string` | — | Pagination token from previous response |
315
+
316
+ **Response** (200):
317
+
318
+ ```json
319
+ {
320
+ "success": true,
321
+ "data": {
322
+ "items": [
323
+ { "bucket": "default", "fullPath": "images/photo.jpg", "name": "photo.jpg" }
324
+ ],
325
+ "prefixes": [
326
+ { "bucket": "default", "fullPath": "images/thumbnails", "name": "thumbnails" }
327
+ ],
328
+ "nextPageToken": "25"
329
+ }
330
+ }
331
+ ```
332
+
333
+ - `items` — files in the prefix
334
+ - `prefixes` — subdirectories/folders in the prefix
335
+ - `nextPageToken` — pass to the next request for pagination (only present if more results exist)
336
+
337
+ For S3, listing uses the `Delimiter: "/"` for folder-like behavior via `ListObjectsV2Command`.
338
+
339
+ ### POST /api/storage/folder
340
+
341
+ Create a new folder.
342
+
343
+ **Request body:**
344
+
345
+ ```json
346
+ {
347
+ "path": "products/images/thumbnails",
348
+ "bucket": "default"
349
+ }
350
+ ```
351
+
352
+ 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`.
353
+
354
+ **Response** (201):
355
+
356
+ ```json
357
+ { "success": true, "message": "Folder created" }
358
+ ```
359
+
360
+ ### DELETE /api/storage/file/{path}
361
+
362
+ 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).
363
+
364
+ **Response** (200):
365
+
366
+ ```json
367
+ { "success": true, "message": "File deleted" }
368
+ ```
369
+
370
+ ## TUS Resumable Uploads
371
+
372
+ 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.
373
+
374
+ ### TUS Configuration
375
+
376
+ | Setting | Value |
377
+ |---------|-------|
378
+ | TUS version | `1.0.0` |
379
+ | Extensions | `creation`, `termination` |
380
+ | Max upload size | 5 GB (`5 * 1024 * 1024 * 1024` bytes) |
381
+ | Stale upload expiry | 24 hours |
382
+ | Cleanup interval | Every 60 seconds |
383
+ | Temp directory | `{storageBasePath}/.tus-uploads` |
384
+
385
+ TUS uploads are stored in a temporary directory and automatically moved to the final storage backend (via `putObject`) when the upload completes.
386
+
387
+ ### TUS Protocol Flow
388
+
389
+ ```
390
+ 1. OPTIONS /api/storage/tus → Discover capabilities
391
+ 2. POST /api/storage/tus → Create upload, get Location header
392
+ 3. PATCH /api/storage/tus/:id → Send chunks (repeat until done)
393
+ 4. (auto) Upload finalized → moved to storage controller
394
+ ```
395
+
396
+ ### OPTIONS /api/storage/tus
397
+
398
+ Returns TUS server capabilities.
399
+
400
+ **Response headers:**
401
+
402
+ ```
403
+ Tus-Resumable: 1.0.0
404
+ Tus-Version: 1.0.0
405
+ Tus-Extension: creation,termination
406
+ Tus-Max-Size: 5368709120
407
+ ```
408
+
409
+ ### POST /api/storage/tus (Create)
410
+
411
+ Create a new resumable upload.
412
+
413
+ **Required request headers:**
414
+
415
+ | Header | Description |
416
+ |--------|-------------|
417
+ | `Upload-Length` | Total file size in bytes (must be > 0 and ≤ 5 GB) |
418
+ | `Upload-Metadata` | TUS metadata as `key base64value,key2 base64value2` |
419
+
420
+ **Supported metadata keys:**
421
+
422
+ | Key | Description |
423
+ |-----|-------------|
424
+ | `filename` | Original file name (used as storage key if no `key` provided) |
425
+ | `key` | Explicit storage key/path |
426
+ | `bucket` | Target bucket |
427
+ | `contentType` | File MIME type |
428
+ | `filetype` | Alternative MIME type key (fallback) |
429
+
430
+ **Response** (201):
431
+
432
+ ```
433
+ Location: http://localhost:3001/api/storage/tus/550e8400-e29b-41d4-a716-446655440000
434
+ Tus-Resumable: 1.0.0
435
+ Upload-Offset: 0
436
+ ```
437
+
438
+ ### HEAD /api/storage/tus/:id (Progress)
439
+
440
+ Query the current upload progress.
441
+
442
+ **Response** (200):
443
+
444
+ ```
445
+ Tus-Resumable: 1.0.0
446
+ Upload-Offset: 1048576
447
+ Upload-Length: 10485760
448
+ Cache-Control: no-store
449
+ ```
450
+
451
+ ### PATCH /api/storage/tus/:id (Append)
452
+
453
+ Append data to an in-progress upload.
454
+
455
+ **Required request headers:**
456
+
457
+ | Header | Value |
458
+ |--------|-------|
459
+ | `Content-Type` | `application/offset+octet-stream` |
460
+ | `Upload-Offset` | Must match the server's current offset |
461
+
462
+ **Error responses:**
463
+
464
+ | Status | Condition |
465
+ |--------|-----------|
466
+ | 400 | Missing `Upload-Offset` header, or upload already completed |
467
+ | 409 | Offset mismatch (client offset ≠ server offset) |
468
+ | 413 | Chunk would exceed declared `Upload-Length` |
469
+ | 415 | Wrong `Content-Type` (must be `application/offset+octet-stream`) |
470
+
471
+ 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.
472
+
473
+ ### DELETE /api/storage/tus/:id (Terminate)
474
+
475
+ Cancel and remove an in-progress upload. Deletes the temp file and removes the upload from the in-memory registry.
476
+
477
+ ### TUS Client Example
478
+
479
+ ```typescript
480
+ // Using tus-js-client (npm install tus-js-client)
481
+ import * as tus from "tus-js-client";
482
+
483
+ const file = document.querySelector<HTMLInputElement>("#fileInput")!.files![0];
484
+
485
+ const upload = new tus.Upload(file, {
486
+ endpoint: "http://localhost:3001/api/storage/tus",
487
+ retryDelays: [0, 1000, 3000, 5000],
488
+ metadata: {
489
+ filename: file.name,
490
+ filetype: file.type,
491
+ key: `uploads/${file.name}`,
492
+ bucket: "default",
493
+ },
494
+ headers: {
495
+ Authorization: `Bearer ${accessToken}`,
496
+ },
497
+ onError: (error) => console.error("Upload failed:", error),
498
+ onProgress: (bytesUploaded, bytesTotal) => {
499
+ const pct = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
500
+ console.log(`${pct}%`);
501
+ },
502
+ onSuccess: () => console.log("Upload complete:", upload.url),
503
+ });
504
+
505
+ upload.start();
506
+ ```
507
+
508
+ ## Image Transformation
509
+
510
+ 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.
511
+
512
+ > **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.
513
+
514
+ ### Transform Query Parameters
515
+
516
+ | Parameter | Type | Range | Default | Description |
517
+ |-----------|------|-------|---------|-------------|
518
+ | `width` | `number` | 1–4096 | — | Target width in pixels |
519
+ | `height` | `number` | 1–4096 | — | Target height in pixels |
520
+ | `quality` | `number` | 1–100 | `80` | Output quality |
521
+ | `format` | `string` | `webp`, `avif`, `jpeg`, `png` | `webp` | Output format |
522
+ | `fit` | `string` | `cover`, `contain`, `fill`, `inside`, `outside` | `cover` | Resize fit mode |
523
+
524
+ All dimensions are capped at **4096 px** to prevent abuse. `withoutEnlargement` is enabled — images are never upscaled.
525
+
526
+ ### Transform Examples
527
+
528
+ ```
529
+ # Resize to 300px wide, auto height, WebP format
530
+ GET /api/storage/file/default/images/photo.jpg?width=300
531
+
532
+ # Resize to 800x600, JPEG at 90% quality
533
+ GET /api/storage/file/default/images/photo.jpg?width=800&height=600&format=jpeg&quality=90
534
+
535
+ # Convert to AVIF with contain fit
536
+ GET /api/storage/file/default/images/photo.jpg?format=avif&fit=contain&width=500&height=500
537
+ ```
538
+
539
+ ### Transformable Image Types
540
+
541
+ Only raster images are transformable. SVG and GIF are **excluded**:
542
+
543
+ ```
544
+ ✅ image/jpeg, image/png, image/webp, image/bmp, image/tiff
545
+ ❌ image/svg+xml, image/gif
546
+ ```
547
+
548
+ ### Transform Cache
549
+
550
+ Transformed images are cached in an **LRU in-memory cache** to avoid redundant processing:
551
+
552
+ | Setting | Value |
553
+ |---------|-------|
554
+ | Max entries | 500 |
555
+ | TTL | 1 hour (3,600,000 ms) |
556
+ | Cache key | `{filePath}::{JSON.stringify(options)}` |
557
+ | Eviction policy | LRU (oldest entry evicted when at capacity) |
558
+
559
+ Transformed responses include `Cache-Control: public, max-age=31536000, immutable`.
560
+
561
+ ## Client SDK Methods
562
+
563
+ 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
+
565
+ ### putObject
566
+
567
+ Upload a file.
568
+
569
+ ```typescript
570
+ const result = await client.storage.putObject({
571
+ file: myFile, // File object
572
+ key: "products/images/photo.jpg", // Storage key/path
573
+ bucket: "default", // Optional bucket
574
+ metadata: { category: "product" }, // Optional custom metadata
575
+ });
576
+ // result: { key: "products/images/photo.jpg", bucket: "default", storageUrl: "local://..." }
577
+ ```
578
+
579
+ The SDK sends a `multipart/form-data` request to `POST /api/storage/upload`. Custom metadata keys are prefixed with `metadata_` in the form data.
580
+
581
+ ### getSignedUrl
582
+
583
+ Get a download URL and metadata for a file. Results are cached in-memory on the client.
584
+
585
+ ```typescript
586
+ const config = await client.storage.getSignedUrl(
587
+ "products/images/photo.jpg", // key or storageUrl (local:// or s3://)
588
+ "default" // optional bucket
589
+ );
590
+
591
+ if (config.fileNotFound) {
592
+ console.log("File does not exist");
593
+ } else {
594
+ console.log(config.url); // Full URL with auth token appended
595
+ console.log(config.metadata); // { bucket, fullPath, name, size, contentType, customMetadata }
596
+ }
597
+ ```
598
+
599
+ The URL is constructed as `{baseUrl}/api/storage/file/{path}?token={accessToken}`.
600
+
601
+ Protocol prefixes (`local://`, `s3://`) are automatically stripped from the key.
602
+
603
+ ### getObject
604
+
605
+ Download a file as a `File` object.
606
+
607
+ ```typescript
608
+ const file = await client.storage.getObject(
609
+ "products/images/photo.jpg", // key or storageUrl
610
+ "default" // optional bucket
611
+ );
612
+
613
+ if (file) {
614
+ console.log(file.name); // "photo.jpg"
615
+ console.log(file.type); // "image/jpeg"
616
+ const blob = new Blob([file]);
617
+ // use blob for display, further processing, etc.
618
+ }
619
+ ```
620
+
621
+ > **IMPORTANT FOR AGENTS:** `getObject` uses raw `fetch` (not the JSON transport) because the response is a binary blob, not JSON.
622
+
623
+ ### deleteObject
624
+
625
+ Delete a file. Silently ignores 404 errors (file already deleted).
626
+
627
+ ```typescript
628
+ await client.storage.deleteObject(
629
+ "products/images/photo.jpg", // key or storageUrl
630
+ "default" // optional bucket
631
+ );
632
+ ```
633
+
634
+ ### listObjects
635
+
636
+ List files and folders in a prefix with pagination.
637
+
638
+ ```typescript
639
+ const result = await client.storage.listObjects("products/images", {
640
+ bucket: "default",
641
+ maxResults: 50,
642
+ pageToken: undefined, // from previous result.nextPageToken
643
+ });
644
+
645
+ console.log(result.items); // files: StorageReference[]
646
+ console.log(result.prefixes); // folders: StorageReference[]
647
+ console.log(result.nextPageToken); // for next page, or undefined
648
+ ```
649
+
650
+ ## Collection File Upload Properties
651
+
652
+ Define file upload fields in your collections using the `storage` option on string properties:
653
+
654
+ ```typescript
655
+ import { PostgresCollection } from "@rebasepro/types";
656
+
657
+ const productsCollection: PostgresCollection = {
658
+ name: "Products",
659
+ table: "products",
660
+ properties: {
661
+ image: {
662
+ name: "Product Image",
663
+ type: "string",
664
+ storage: {
665
+ storagePath: "products/images",
666
+ acceptedFiles: ["image/*"],
667
+ maxSize: 5 * 1024 * 1024, // 5MB
668
+ }
669
+ },
670
+ documents: {
671
+ name: "Documents",
672
+ type: "array",
673
+ of: {
674
+ type: "string",
675
+ storage: {
676
+ storagePath: "products/documents",
677
+ acceptedFiles: ["application/pdf", "application/msword"],
678
+ }
679
+ }
680
+ }
681
+ }
682
+ };
683
+ ```
684
+
685
+ ## Storage Browser (Studio)
686
+
687
+ The `@rebasepro/studio` package includes a built-in `StorageView` component:
688
+ - Browse uploaded files and folders with a tree sidebar
689
+ - Drag-and-drop file uploads
690
+ - Image, video, and audio previews with metadata
691
+ - File search and filtering
692
+ - Grid and list view modes
693
+
694
+ ## Validation and Error Handling
695
+
696
+ ### File Size Validation
697
+
698
+ File size is validated at two levels:
699
+
700
+ 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).
701
+ 2. **Controller-level validation** — the controller's `validateFile()` checks `file.size` against `maxFileSize` and throws an `Error` if exceeded.
702
+
703
+ ### MIME Type Validation
704
+
705
+ When `allowedMimeTypes` is set in the storage config, uploads with disallowed MIME types are rejected with an error:
706
+
707
+ ```
708
+ File type application/zip is not allowed. Allowed types: image/jpeg, image/png
709
+ ```
710
+
711
+ ### Common MIME Type Constants
712
+
713
+ The storage module exports two convenience arrays:
714
+
715
+ ```typescript
716
+ import { IMAGE_MIME_TYPES, DOCUMENT_MIME_TYPES } from "@rebasepro/server-core";
717
+
718
+ // IMAGE_MIME_TYPES:
719
+ // ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml", "image/bmp", "image/tiff"]
720
+
721
+ // DOCUMENT_MIME_TYPES:
722
+ // ["application/pdf", "application/msword",
723
+ // "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
724
+ // "application/vnd.ms-excel",
725
+ // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
726
+ // "application/vnd.ms-powerpoint",
727
+ // "application/vnd.openxmlformats-officedocument.presentationml.presentation",
728
+ // "text/plain", "text/csv"]
729
+ ```
730
+
731
+ ### TUS Error Codes
732
+
733
+ | HTTP Status | Condition |
734
+ |-------------|-----------|
735
+ | 400 | Missing/invalid `Upload-Length` header, missing `Upload-Offset`, upload already completed |
736
+ | 404 | Upload ID not found |
737
+ | 409 | `Upload-Offset` mismatch (client offset ≠ server offset) |
738
+ | 413 | `Upload-Length` exceeds 5 GB max, or chunk exceeds declared length |
739
+ | 415 | `Content-Type` is not `application/offset+octet-stream` |
740
+
741
+ ## Metadata Sidecar Files (Local Storage)
742
+
743
+ For local storage, every uploaded file has a companion `.metadata.json` file saved alongside it. This sidecar file stores:
744
+
745
+ ```json
746
+ {
747
+ "contentType": "image/jpeg",
748
+ "size": 204800,
749
+ "uploadedAt": "2026-01-15T10:30:00.000Z",
750
+ "category": "product"
751
+ }
752
+ ```
753
+
754
+ - `contentType` and `size` are always present
755
+ - Custom metadata from the upload request is merged in
756
+ - The content type from this file is used when serving files via `GET /file/*`
757
+ - When a file is deleted, its `.metadata.json` is also deleted
758
+ - Metadata files are skipped when listing directory contents (filtered out by name pattern)
759
+
760
+ ## References
761
+
762
+ - **Documentation:** [rebase.pro/docs](https://rebase.pro/docs)
763
+ - **GitHub:** [github.com/rebasepro/rebase](https://github.com/rebasepro/rebase)
764
+ - **TUS Protocol:** [tus.io/protocols/resumable-upload](https://tus.io/protocols/resumable-upload)
765
+ - **Sharp (image transforms):** [sharp.pixelplumbing.com](https://sharp.pixelplumbing.com)