This is a demo site.Purchase now

S3 file storage

S3-compatible file storage integration for uploads and downloads

The boilerplate includes a complete S3 file storage implementation that works with AWS S3, Cloudflare R2, MinIO, and other S3-compatible services. It provides secure file uploads and downloads using signed URLs.

Environment variables

Configure your S3-compatible storage provider in .env:

# S3-compatible storage
S3_ENDPOINT=https://your-s3-endpoint.com
S3_REGION=auto
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_BUCKET=your-bucket-name

Provider examples

AWS S3:

S3_ENDPOINT=https://s3.amazonaws.com
S3_REGION=us-east-1

Cloudflare R2:

S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_REGION=auto

MinIO:

S3_ENDPOINT=https://minio.yourdomain.com
S3_REGION=us-east-1
Cloudflare R2 is recommended for its zero egress fees and S3-compatible API.

Service functions

The implementation consists of three layers of services:

Server-side services

server/services/storage-server-service.ts - High-level storage operations:

// Get signed URL for uploading a file
await getSignedUploadUrl(key, contentType, bucket?)

// Get signed URL for downloading a file
await getSignedDownloadUrl(key, expiresIn?, filename?, bucket?)

server/utils/signed-url.ts - Low-level signed URL generation:

// Generate signed upload URL (default: 2 minutes expiry)
await getSignedUploadUrl(s3Client, bucket, key, contentType, expiresIn?)

// Generate signed download URL (default: 1 hour expiry)
await getSignedDownloadUrl(s3Client, bucket, key, expiresIn?, filename?)

Client-side services

app/services/storage-client-service.ts - Client API for file operations:

// Upload file with progress tracking
await uploadFileWithSignedUrl(file, contentType, onProgress?, customKey?)

// Download file (triggers browser download)
await downloadFileWithSignedUrl(key, filename?)

// Generate unique storage key
generateStorageKey(filename?)

Signed URLs vs. public URLs

Signed URLs (default)

The boilerplate uses signed URLs for secure, temporary access:

  • Secure - No public access to files
  • Temporary - URLs expire after a set time (configurable)
  • Access control - Server validates permissions before generating URLs
  • Private buckets - Bucket remains private, files not publicly accessible

Upload flow:

  1. Client requests signed upload URL from server
  2. Server generates temporary URL with write permissions
  3. Client uploads directly to S3 using signed URL
  4. Client saves file metadata to database

Download flow:

  1. Client requests signed download URL from server
  2. Server validates user permissions
  3. Server generates temporary URL with read permissions
  4. Client downloads file using signed URL

Public URLs

For public files (avatars, product images, etc.), you can use public buckets or CDN URLs:

const publicUrl = `https://cdn.yourdomain.com/${key}`

Public URLs don't require server-side URL generation but sacrifice access control.

Database metadata

Files are tracked in the database with the File model:

model File {
  id        String   @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  key       String   @unique
  name      String
  mimeType  String
  size      BigInt
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  userId    String   @db.Uuid
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("file")
}

Key points:

  • key is the S3 object key (unique identifier in bucket)
  • size uses BigInt for large files
  • User relationship for access control
  • Metadata is searchable via Prisma queries

Usage example

Upload a file

import { uploadFileWithSignedUrl } from '@/services/storage-client-service'
import { createFile } from '@/services/files-client-service'

const file = event.target.files[0]

// Upload to S3
const { key } = await uploadFileWithSignedUrl(
  file,
  file.type,
  (progress) => console.log(`${progress}% uploaded`)
)

// Save metadata
await createFile({
  key,
  name: file.name,
  size: file.size,
  mimeType: file.type,
  userId: currentUser.id,
})

Download a file

import { downloadFileWithSignedUrl } from '@/services/storage-client-service'

// Triggers browser download
await downloadFileWithSignedUrl(file.key, file.name)

Complete implementation

For a full implementation example with drag-and-drop uploads, file tables, and metadata management, see the S3 file upload/download template.

Reference