Skip to main content

Prerequisites

  • AWS account with S3 access
  • IAM user with S3 permissions

1. Create S3 Bucket

  1. Go to AWS S3 Console
  2. Click Create bucket
  3. Configure:
    • Bucket name: your-app-uploads (must be globally unique)
    • Region: Choose closest to your users
    • Block Public Access: Keep enabled (we use presigned URLs)
  4. Click Create bucket

CORS Configuration

Add this CORS policy to your bucket:
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
The ExposeHeaders: ["ETag"] is required for multipart uploads to work correctly.

2. Create IAM User

  1. Go to IAM Console
  2. Create a new user with Programmatic access
  3. Attach this policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:ListBucket",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts"
      ],
      "Resource": [
        "arn:aws:s3:::your-app-uploads",
        "arn:aws:s3:::your-app-uploads/*"
      ]
    }
  ]
}
  1. Save the Access Key ID and Secret Access Key

3. Environment Variables

Add these to your backend .env:
# AWS S3 Configuration
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AWS_REGION=us-east-1
AWS_S3_BUCKET=your-app-uploads

4. API Endpoints

The upload router (apps/api/src/routers/upload.ts) provides these endpoints:

Simple Upload

EndpointDescription
requestUploadUrlGet presigned URL for direct upload
confirmUploadMark upload as complete

Multipart Upload

EndpointDescription
initiateMultipartStart multipart upload, get upload ID
getPartUrlGet presigned URL for a specific part
completePartRecord completed part with ETag
completeMultipartFinalize multipart upload
abortMultipartCancel and cleanup failed upload

5. Database Schema

The File model tracks uploads:
model File {
  id            String    @id @default(cuid())
  userId        String
  filename      String
  mimeType      String
  size          Int
  key           String    @unique
  url           String?
  isUploaded    Boolean   @default(false)

  // Multipart tracking
  uploadId      String?
  totalParts    Int?
  uploadedParts Json?

  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  user          User      @relation(fields: [userId], references: [id])
}

6. Storage Quota

Configure per-user storage limits in upload.ts:
// Maximum storage per user (default: 500MB)
const STORAGE_QUOTA = 500 * 1024 * 1024;

// For testing, you can increase this
const STORAGE_QUOTA = 5 * 1024 * 1024 * 1024; // 5GB

Security Considerations

URLs expire after 1 hour by default. Adjust expiresIn in s3.ts if needed.
Allowed MIME types are configured in ALLOWED_MIME_TYPES. Add/remove as needed.
Consider adding rate limiting to upload endpoints in production.
For user-generated content, consider AWS Lambda + ClamAV for scanning.

Test Checklist

  • API starts without S3 errors
  • requestUploadUrl returns a presigned URL
  • Uploaded file can be confirmed in the database

Troubleshooting

If uploads fail, re-check S3 credentials and bucket CORS settings.

Remove / Disable

To disable uploads while you configure S3, set: apps/mobile/features/feature-registry.tsxfeatureFlags.fileUploads = false For production removal guidance, see Removing Features.