@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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/package.json +21 -0
- package/skills/rebase-admin/SKILL.md +816 -0
- package/skills/rebase-api/SKILL.md +673 -0
- package/skills/rebase-api/references/.gitkeep +3 -0
- package/skills/rebase-auth/SKILL.md +1323 -0
- package/skills/rebase-auth/references/.gitkeep +3 -0
- package/skills/rebase-backend-postgres/SKILL.md +633 -0
- package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
- package/skills/rebase-basics/SKILL.md +757 -0
- package/skills/rebase-basics/references/.gitkeep +3 -0
- package/skills/rebase-collections/SKILL.md +1421 -0
- package/skills/rebase-collections/references/.gitkeep +3 -0
- package/skills/rebase-cron-jobs/SKILL.md +704 -0
- package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
- package/skills/rebase-custom-functions/SKILL.md +281 -0
- package/skills/rebase-deployment/SKILL.md +894 -0
- package/skills/rebase-deployment/references/.gitkeep +3 -0
- package/skills/rebase-design-language/SKILL.md +692 -0
- package/skills/rebase-email/SKILL.md +701 -0
- package/skills/rebase-email/references/.gitkeep +1 -0
- package/skills/rebase-entity-history/SKILL.md +485 -0
- package/skills/rebase-entity-history/references/.gitkeep +1 -0
- package/skills/rebase-local-env-setup/SKILL.md +189 -0
- package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
- package/skills/rebase-realtime/SKILL.md +759 -0
- package/skills/rebase-realtime/references/.gitkeep +3 -0
- package/skills/rebase-sdk/SKILL.md +617 -0
- package/skills/rebase-sdk/references/.gitkeep +0 -0
- package/skills/rebase-security/SKILL.md +646 -0
- package/skills/rebase-storage/SKILL.md +989 -0
- package/skills/rebase-storage/references/.gitkeep +3 -0
- package/skills/rebase-studio/SKILL.md +772 -0
- package/skills/rebase-studio/references/.gitkeep +3 -0
- package/skills/rebase-ui-components/SKILL.md +1579 -0
- package/skills/rebase-ui-components/references/.gitkeep +3 -0
- package/skills/rebase-webhooks/SKILL.md +618 -0
- 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)
|