Storage
Configure file storage using split-package architecture
LaunchSaaS provides file storage support via a split-package design. The core @launchsaas/storage package defines the interface and Storage service; each provider is a separate package that you install only when needed.
Updated: 2026-03-15
Architecture
@launchsaas/storage ← interface + Storage class (always needed)
@launchsaas/storage-s3 ← S3-compatible (AWS S3, Cloudflare R2, MinIO, …)You only add the provider package you actually use.
LaunchSaaS ships with @launchsaas/storage-s3 as the default. In your own
app, install it if you need storage, or skip Storage.init() entirely to
leave storage disabled.
Setting Up Storage
1. Install the provider package
# S3-compatible (AWS S3, Cloudflare R2, MinIO, …)
pnpm add @launchsaas/storage-s32. Add env variables
Add the provider's keys export to your app's env.ts:
// src/env.ts
import { keys as storageKeys } from "@launchsaas/storage-s3";
export const env = createEnv({
extends: [
// ... other keys
storageKeys,
],
// ...
});Then set the variables in your .env:
S3_REGION="auto"
S3_BUCKET="your-bucket-name"
S3_ACCESS_KEY_ID="your-access-key-id"
S3_SECRET_ACCESS_KEY="your-secret-access-key"
S3_API_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
S3_PUBLIC_ENDPOINT="https://pub-<id>.r2.dev"3. Initialize in instrumentation.ts
Uncomment the storage block in src/instrumentation.ts:
// src/instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { Storage } = await import("@launchsaas/storage");
const { S3StorageProvider } = await import("@launchsaas/storage-s3");
Storage.init(S3StorageProvider.create());
}
}Environment Variables
| Variable | Description |
|---|---|
S3_REGION | Region (e.g., us-east-1, auto for R2) |
S3_BUCKET | Bucket name |
S3_ACCESS_KEY_ID | Access key ID |
S3_SECRET_ACCESS_KEY | Secret access key |
S3_API_ENDPOINT | S3 API endpoint URL |
S3_PUBLIC_ENDPOINT | Public base URL for accessing uploaded files |
Provider Setup
Cloudflare R2 (Recommended)
- Go to Cloudflare Dashboard → R2
- Create a bucket
- Create an API token with R2 permissions
- Add the env vars (see above)
AWS S3
S3_REGION="us-east-1"
S3_BUCKET="your-bucket-name"
S3_ACCESS_KEY_ID="your-access-key-id"
S3_SECRET_ACCESS_KEY="your-secret-access-key"
S3_API_ENDPOINT="https://s3.us-east-1.amazonaws.com"
S3_PUBLIC_ENDPOINT="https://your-bucket-name.s3.us-east-1.amazonaws.com"MinIO (Self-Hosted)
S3_REGION="us-east-1"
S3_BUCKET="your-bucket-name"
S3_ACCESS_KEY_ID="your-access-key"
S3_SECRET_ACCESS_KEY="your-secret-key"
S3_API_ENDPOINT="https://your-minio-server.com"
S3_PUBLIC_ENDPOINT="https://your-minio-server.com/your-bucket-name"CORS Configuration
For browser uploads, configure CORS on your bucket:
[
{
"AllowedOrigins": ["https://yourdomain.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]Custom Storage Provider
To use a storage service not covered by the built-in S3 provider, create a new storage-xxx package:
1. Create the package
mkdir -p packages/storage-myservice/srcpackages/storage-myservice/package.json:
{
"name": "@launchsaas/storage-myservice",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@launchsaas/errors": "workspace:*",
"@launchsaas/storage": "workspace:*",
"@t3-oss/env-nextjs": "^0.13.10",
"my-storage-sdk": "^1.0.0",
"zod": "^4.0.0"
}
}packages/storage-myservice/src/keys.ts:
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const keys = createEnv({
server: {
MYSERVICE_API_KEY: z.string().optional(),
MYSERVICE_BUCKET: z.string().optional(),
},
experimental__runtimeEnv: {},
});packages/storage-myservice/src/provider.ts:
import type {
DeleteOptions,
StorageProvider,
UploadOptions,
} from "@launchsaas/storage";
export class MyServiceStorageProvider implements StorageProvider {
readonly name = "myservice";
static create(): MyServiceStorageProvider {
return new MyServiceStorageProvider();
}
async upload(options: UploadOptions): Promise<{ url: string }> {
// Implement upload
return { url: "https://example.com/file" };
}
async delete(options: DeleteOptions): Promise<void> {
// Implement delete
}
getPublicUrl(key: string): string {
return `https://example.com/${key}`;
}
getKeyFromUrl(url: string): string {
return url.replace("https://example.com/", "");
}
}packages/storage-myservice/src/index.ts:
export { MyServiceStorageProvider } from "./provider";
export { keys } from "./keys";2. Wire up in your app
pnpm add @launchsaas/storage-myserviceAdd keys to env.ts and initialize in instrumentation.ts:
export async function register() {
const { Storage } = await import("@launchsaas/storage");
const { MyServiceStorageProvider } =
await import("@launchsaas/storage-myservice");
Storage.init(MyServiceStorageProvider.create());
}Disabling Storage
To disable storage entirely, don't call Storage.init() in instrumentation.ts. Storage.enabled() will return false and actions will fail fast with a clear error.