File Storage
Radiant provides a pluggable storage system for handling file uploads. The default LocalStorageProvider saves files to disk. For production, swap in the S3 plugin to store files in cloud storage.
How Uploads Work
When a collection has an upload field, the runtime auto-generates a global upload endpoint:
POST /api/upload → Upload a file
GET /api/uploads/:filename → Serve an uploaded file
The upload endpoint accepts multipart/form-data with a file field and returns metadata about the stored file.
Uploading a File
curl -X POST http://localhost:3000/api/upload \
-F "file=@photo.jpg"
Response:
{
"filename": "a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg",
"originalName": "photo.jpg",
"mimetype": "image/jpeg",
"size": 2048576,
"url": "/api/uploads/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg"
}
Default: Local Storage
By default, Radiant uses LocalStorageProvider which saves files to an uploads/ directory on disk:
// This is the default — no configuration needed
export const app = createRadiant({
adapter: sqlite({ url: process.env.DATABASE_URL! }),
});
Files are served at /api/uploads/:filename. The upload directory is created automatically.
S3 Storage Plugin
For production, use the S3 plugin to store files in AWS S3, MinIO, DigitalOcean Spaces, or any S3-compatible service.
Installation
bun add @codesordinatestudio/radiant-plugin-s3
Usage
import { createRadiant } from "../radiant/gen";
import { sqlite } from "@codesordinatestudio/radiant-plugin-sqlite";
import { s3Storage } from "@codesordinatestudio/radiant-plugin-s3";
export const app = createRadiant({
adapter: sqlite({ url: process.env.DATABASE_URL! }),
storage: s3Storage({
bucket: "my-app-uploads",
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
publicUrl: "https://cdn.myapp.com", // optional CDN URL
}),
});
S3 with MinIO
import { s3Storage } from "@codesordinatestudio/radiant-plugin-s3";
storage: s3Storage({
bucket: "uploads",
region: "us-east-1",
endpoint: "http://localhost:9000",
forcePathStyle: true, // required for MinIO
accessKeyId: process.env.MINIO_ACCESS_KEY!,
secretAccessKey: process.env.MINIO_SECRET_KEY!,
})
The StorageProvider Interface
Both local and S3 storage implement the StorageProvider interface:
interface StorageProvider {
saveFile(file: File, options?: { filename?: string }): Promise<UploadedFile>;
deleteFile(filename: string): Promise<void>;
}
interface UploadedFile {
filename: string;
originalName: string;
mimetype: string;
size: number;
url: string;
}
Writing a Custom Storage Provider
Implement StorageProvider to use any storage backend — Cloudflare R2, Azure Blob, Google Cloud Storage, etc:
import type { StorageProvider, UploadedFile } from "@codesordinatestudio/radiant-bun";
class R2StorageProvider implements StorageProvider {
// ... your R2 implementation ...
async saveFile(file: File): Promise<UploadedFile> {
// Upload to R2
// Return the file metadata
}
async deleteFile(filename: string): Promise<void> {
// Delete from R2
}
}
// Register it
export const app = createRadiant({
adapter: sqlite({ url: process.env.DATABASE_URL! }),
storage: new R2StorageProvider(),
});
Using Uploads in Collections
Declare an upload field in your schema to store file metadata:
collection products {
fields: {
name: text
image: upload @optional
gallery: upload[] @optional
}
}
When you create or update a record, pass the upload response's filename or url as the field value:
# 1. Upload the file
curl -X POST /api/upload -F "file=@product.jpg"
# → { "filename": "abc-123.jpg", "url": "/api/uploads/abc-123.jpg", ... }
# 2. Create the product with the uploaded file URL
curl -X POST /api/products \
-H "Content-Type: application/json" \
-d '{"name": "Widget", "image": "abc-123.jpg"}'
Related
- Plugins — All available plugins
- Collections — Defining
uploadfields in the DSL