Storage

S3 Storage

S3-compatible storage provider — upload, delete, presigned URLs for AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO.

The @commercejs/storage-s3 package implements the StorageProvider interface for S3-compatible object storage. It powers product image uploads, asset management, and file serving for Commerce.js Cloud's native platform.

!NOTE External platform adapters (Salla, Medusa) manage their own media — this provider is for the native Commerce.js platform where Commerce.js is the system of record.

Installation

pnpm add @commercejs/storage-s3

Quick Start

import { S3StorageProvider } from '@commercejs/storage-s3'

const storage = new S3StorageProvider({
  endpoint: 'https://s3.us-east-1.amazonaws.com',
  region: 'us-east-1',
  bucket: 'my-store-assets',
  accessKeyId: process.env.S3_ACCESS_KEY_ID!,
  secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
  publicUrl: 'https://cdn.example.com',
})

// Upload a product image
const result = await storage.upload({
  filename: 'hero.jpg',
  mimeType: 'image/jpeg',
  content: imageBytes,
  directory: 'products',
})
// → { key: 'products/hero-a1b2c3d4.jpg', url: 'https://cdn.example.com/products/hero-a1b2c3d4.jpg' }

Configuration

OptionTypeRequiredDescription
endpointstringS3-compatible endpoint URL
regionstringRegion (e.g., 'us-east-1', 'auto' for R2)
bucketstringBucket name
accessKeyIdstringAccess key ID
secretAccessKeystringSecret access key
publicUrlstringCDN base URL for public files
prefixstringGlobal key prefix (e.g., 'uploads/')
forcePathStylebooleanPath-style URLs (required for MinIO)

Methods

All methods follow the StorageProvider interface from @commercejs/types.

upload

Uploads a file and returns the storage key + public URL:

const result = await storage.upload({
  filename: 'hero.jpg',
  mimeType: 'image/jpeg',
  content: imageBytes,       // Uint8Array
  directory: 'products',     // optional path prefix
})

console.log(result.key) // 'products/hero-a1b2c3d4.jpg'
console.log(result.url) // 'https://cdn.example.com/products/hero-a1b2c3d4.jpg'

Keys are auto-generated with a random suffix to prevent collisions.

delete

Deletes a file by its storage key:

await storage.delete('products/hero-a1b2c3d4.jpg')

getUrl

Returns the public/CDN URL for a stored file:

const url = storage.getUrl('products/hero-a1b2c3d4.jpg')
// With publicUrl → 'https://cdn.example.com/products/hero-a1b2c3d4.jpg'
// Without       → 'https://bucket.s3.us-east-1.amazonaws.com/products/hero-a1b2c3d4.jpg'

getPresignedUploadUrl

Generates a presigned PUT URL for direct browser-to-S3 uploads:

const { url, headers } = await storage.getPresignedUploadUrl('products/hero.jpg', {
  contentType: 'image/jpeg',
  expiresIn: 600, // 10 minutes
})

// In the browser:
await fetch(url, { method: 'PUT', headers, body: file })

getPresignedDownloadUrl

Generates a presigned GET URL for private file downloads:

const { url } = await storage.getPresignedDownloadUrl('invoices/inv-001.pdf', {
  expiresIn: 900, // 15 minutes
})

S3-Compatible Services

The provider works with any S3-protocol service by changing the endpoint:

// AWS S3
new S3StorageProvider({ endpoint: 'https://s3.us-east-1.amazonaws.com', region: 'us-east-1', ... })

// Cloudflare R2
new S3StorageProvider({ endpoint: 'https://<account>.r2.cloudflarestorage.com', region: 'auto', ... })

// DigitalOcean Spaces
new S3StorageProvider({ endpoint: 'https://nyc3.digitaloceanspaces.com', region: 'nyc3', ... })

// MinIO (self-hosted)
new S3StorageProvider({ endpoint: 'http://localhost:9000', forcePathStyle: true, ... })

Using with the Orchestrator

import { createCommerce } from '@commercejs/core'
import { S3StorageProvider } from '@commercejs/storage-s3'

const commerce = createCommerce({
  adapter,
  storage: new S3StorageProvider({
    endpoint: 'https://s3.us-east-1.amazonaws.com',
    region: 'us-east-1',
    bucket: 'my-store',
    accessKeyId: process.env.S3_ACCESS_KEY_ID!,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
    publicUrl: 'https://cdn.example.com',
  }),
})

// Upload through the orchestrator
const result = await commerce.uploadFile({
  filename: 'hero.jpg',
  mimeType: 'image/jpeg',
  content: imageBytes,
  directory: 'products',
})

// Get a presigned upload URL for client-side uploads
const { url, headers } = await commerce.getPresignedUploadUrl('products/hero.jpg', {
  contentType: 'image/jpeg',
})