LogoLaunchSaaS

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-s3

2. 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

VariableDescription
S3_REGIONRegion (e.g., us-east-1, auto for R2)
S3_BUCKETBucket name
S3_ACCESS_KEY_IDAccess key ID
S3_SECRET_ACCESS_KEYSecret access key
S3_API_ENDPOINTS3 API endpoint URL
S3_PUBLIC_ENDPOINTPublic base URL for accessing uploaded files

Provider Setup

  1. Go to Cloudflare Dashboard → R2
  2. Create a bucket
  3. Create an API token with R2 permissions
  4. 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/src

packages/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-myservice

Add 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.

References