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

  1. Frontend requests a signed upload URL from the backend.
  2. Backend validates permissions and generates a presigned URL.
  3. Browser uploads the file directly to S3 (no backend bandwidth used).
  4. For public files, the public URL is returned immediately.
  5. 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 -d

This 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:9000

Production (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 S3

Public vs private files

TypeBucketAccessUse case
PublicS3_BUCKET_PUBLICAnonymous read via direct URLProfile images, logos
PrivateS3_BUCKET_PRIVATESigned URLs with 1-hour expiryDocuments, 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

VariableRequiredDescription
S3_BUCKET_PUBLICYesBucket name for public files
S3_BUCKET_PRIVATEYesBucket name for private files
S3_ACCESS_KEY_IDYesS3 access key
S3_SECRET_ACCESS_KEYYesS3 secret key
S3_REGIONNoS3 region (default: us-east-1)
S3_ENDPOINTNoCustom endpoint for S3-compatible services

Key files

FileDescription
backend/src/features/file/uploadRouter.tsUpload route handler + S3 client
frontend/src/shared/components/upload-dropzone.tsxDrag-and-drop upload component
packages/upload/docker-compose.ymlMinIO Docker Compose for local dev