File uploads
Your project supports file and image uploads to any S3-compatible storage (AWS S3, MinIO, Cloudflare R2, Backblaze B2). Uploads use signed URLs for direct browser-to-storage transfers with support for public and private files.
How it works
- Frontend requests a signed upload URL from the backend.
- Backend validates permissions and generates a presigned URL.
- Browser uploads the file directly to S3 (no backend bandwidth used).
- For public files, the public URL is returned immediately.
- For private files, a signed download URL (1-hour expiry) is generated on demand.
Files larger than 50 MB automatically use multipart uploads for improved reliability.
Storage configuration
Local development (MinIO)
Start MinIO with Docker Compose from the packages/upload directory:
docker-compose up -dThis creates two buckets (project-public and project-private), sets up an application user, and configures bucket policies.
Add to packages/backend/.env:
S3_BUCKET_PUBLIC=project-public
S3_BUCKET_PRIVATE=project-private
S3_ACCESS_KEY_ID=app_user
S3_SECRET_ACCESS_KEY=app_password123
S3_REGION=us-east-1
S3_ENDPOINT=http://localhost:9000Production (AWS S3)
S3_BUCKET_PUBLIC=your-public-bucket
S3_BUCKET_PRIVATE=your-private-bucket
S3_ACCESS_KEY_ID=AKIA...
S3_SECRET_ACCESS_KEY=...
S3_REGION=us-east-1
# Omit S3_ENDPOINT for AWS S3Public vs private files
| Type | Bucket | Access | Use case |
|---|---|---|---|
| Public | S3_BUCKET_PUBLIC | Anonymous read via direct URL | Profile images, logos |
| Private | S3_BUCKET_PRIVATE | Signed URLs with 1-hour expiry | Documents, sensitive files |
The storage type is configured per field in the permissions/storage configuration.
File organization
Files are stored with this key structure:
{folder}/{organizationId}/{randomUUID}/{originalFileName.ext}The random UUID prevents overwrites and adds security through obscurity.
Permissions
Upload permissions are defined in the storage configuration. Each upload route checks:
- User is authenticated (has a current user, member, and organization)
- User has the required permission for that storage route
Environment variables
| Variable | Required | Description |
|---|---|---|
S3_BUCKET_PUBLIC | Yes | Bucket name for public files |
S3_BUCKET_PRIVATE | Yes | Bucket name for private files |
S3_ACCESS_KEY_ID | Yes | S3 access key |
S3_SECRET_ACCESS_KEY | Yes | S3 secret key |
S3_REGION | No | S3 region (default: us-east-1) |
S3_ENDPOINT | No | Custom endpoint for S3-compatible services |
Key files
| File | Description |
|---|---|
backend/src/features/file/uploadRouter.ts | Upload route handler + S3 client |
frontend/src/shared/components/upload-dropzone.tsx | Drag-and-drop upload component |
packages/upload/docker-compose.yml | MinIO Docker Compose for local dev |