OSMO
DocsBlogChangelog
Configuration/File Upload Adapters

API

  • POSTCreate Testimonial
  • GETList Testimonials

Components

  • Auth Components
  • Contact Components
  • Dialog Manager
  • Layout Components
  • Markdown Components
  • Changelog System

Configuration

  • File Upload Adapters

Developer

  • Safe Actions
  • Zod Route

Guide

  • Getting Started
  • Embedding Testimonials

OSMO

The operating system of the structured products desk: pricing comparison, RFQ pipeline, blotter, settlement and P&L in a single source of truth

Product

BlogDocumentationDashboardAccount

Company

AboutContact

Legal

TermsPrivacy

Montevideo, Uruguay

© 2025 Latin Securities. All rights reserved.

File Upload Adapters

Store uploaded media on any S3-compatible storage (AWS S3, Cloudflare R2, Scaleway, SeaweedFS, MinIO)

Uploaded media are stored on an S3-compatible bucket. The upload system uses an adapter pattern, so swapping the provider never changes application code.

How It Works

The contract lives in src/lib/files/upload-file.ts:

export type UploadFileAdapter = {
  uploadFile: (params: {
    file: File;
    path: string;
  }) => Promise<
    { error: null; data: { url: string } } | { error: Error; data: null }
  >;
  uploadFiles: (
    params: { file: File; path: string }[],
  ) => Promise<{ error: Error | null; data: { url: string } | null }[]>;
};

The active adapter is src/lib/files/s3-adapter.ts, imported in src/features/images/upload-image.action.ts.

Configuration

Add the credentials to your .env:

S3_ENDPOINT="https://s3.example.com"   # or https://<account-id>.r2.cloudflarestorage.com
S3_BUCKET="my-bucket"
S3_ACCESS_KEY="..."
S3_SECRET_KEY="..."

# Optional — only when the provider requires a specific region
# S3_REGION="auto"                     # Cloudflare R2

These are validated in src/lib/env.ts and are required: the app refuses to boot without them.

Notes per provider

  • Cloudflare R2: S3_REGION="auto" is mandatory, otherwise every request fails with SignatureDoesNotMatch.
  • AWS S3 / Scaleway / OVH / DigitalOcean: set S3_REGION to the bucket region.
  • SeaweedFS / MinIO: the default us-east-1 works.

The client uses forcePathStyle, so public URLs are ${S3_ENDPOINT}/${S3_BUCKET}/${key}. The bucket (or the relevant prefix) must allow public reads for the returned URLs to be displayable.

Object Keys

Keys are built from the path passed by the caller plus a nanoid prefix, so two uploads of photo.png never overwrite each other:

orgs/{orgId}/V1StGXR8Z5-photo.png
users/{userId}/V1StGXR8Z5-avatar.png

File names coming from the client are sanitized (only a-zA-Z0-9._- survives), which keeps an upload from escaping its prefix.

Uploading

Never call the adapter from a component — go through the server actions, which validate the file (images only, max 2 MB) and scope the path to the current user or organization:

import {
  uploadOrgImageAction,
  uploadUserImageAction,
} from "@/features/images/upload-image.action";

Using Another Provider

Create an adapter implementing UploadFileAdapter (for example src/lib/files/uploadthing-adapter.ts) and change the import in src/features/images/upload-image.action.ts:

import { fileAdapter } from "@/lib/files/s3-adapter";

Enabling Image Upload

Image upload is gated by a feature flag. In src/site-config.ts:

export const SiteConfig = {
  // ...
  features: {
    enableImageUpload: true,
    // ...
  },
};

This enables drag-and-drop and click-to-upload throughout the app.

Changelog SystemSafe Actions

On This Page

How It WorksConfigurationNotes per providerObject KeysUploadingUsing Another ProviderEnabling Image Upload
Sign in