Storage Overview

Provider-agnostic file storage for Nebutra — presigned URLs, multipart uploads, and resumable uploads via @nebutra/uploads.

Nebutra provides a unified file storage abstraction through @nebutra/uploads. Application code calls a single API regardless of whether files are stored on AWS S3, Cloudflare R2, or any S3-compatible backend. Switching providers requires only environment variable changes — no code modifications.

Architecture

┌──────────────────────────────────────┐
│           Browser / Client           │
│  (React, mobile app, CLI)            │
└──────────────┬───────────────────────┘
               │ 1. Request upload URL
┌──────────────▼───────────────────────┐
│         API Route / Server Action    │
│   getUploadProvider() → presigned    │
│   URL or multipart session           │
└──────────────┬───────────────────────┘
               │ 2. Signed URL returned to client
┌──────────────▼───────────────────────┐
│         Client uploads directly      │
│   PUT {presignedUrl} — no proxy      │
└──────────────┬───────────────────────┘
               │ 3. File lands in bucket
┌──────────────▼───────────────────────┐
│     AWS S3 / Cloudflare R2 / MinIO   │
│     Bucket: nebutra-uploads          │
└──────────────┬───────────────────────┘
               │ 4. Served via CDN
┌──────────────▼───────────────────────┐
│        CloudFront / R2 Public URL    │
│        (getSignedUrl for private)    │
└──────────────────────────────────────┘

Clients upload directly to the storage provider — files never pass through your application server. This keeps upload throughput high and server costs low.

Upload strategies

Choose the right strategy based on file size and reliability requirements:

StrategyBest forSize limitResumable
Presigned URLDocuments, images, small filesUp to ~100 MBNo
Multipart uploadVideos, large archivesUnlimitedPartial
Tus resumableUnreliable connections, large filesUnlimitedYes

Presigned URL

The simplest approach. Your server generates a short-lived signed URL; the client uploads directly with a single PUT request. Ideal for files under 100 MB.

Multipart upload

Splits large files into smaller parts (minimum 5 MB each) and uploads them in parallel. Failed parts can be retried individually. Required for files larger than 5 GB on AWS S3.

Tus resumable

An open protocol for resumable uploads. If the connection drops, the upload resumes from the last acknowledged byte. Best for large files on unreliable networks.

Tus resumable upload support depends on your storage provider configuration. AWS S3 and Cloudflare R2 support Tus via a proxy layer. See Uploads for setup details.

Tenant isolation

Every file is stored under a key prefix that includes the tenant identifier:

{tenantId}/{path/to/file.ext}

# Examples
org_acme/docs/q4-report.pdf
org_beta/avatars/user_123.jpg
org_acme/videos/demo.mp4

This convention ensures tenant files are logically isolated within a shared bucket. Combined with IAM policies or Cloudflare R2 access controls, it also enforces access isolation at the infrastructure level.

Always derive the tenantId from the authenticated session on the server. Never accept a tenantId from the client request body — it can be spoofed.

Quick start

import { getUploadProvider } from "@nebutra/uploads";

const uploads = await getUploadProvider();

// Generate a presigned URL for client-side upload
const { url, headers } = await uploads.createPresignedUpload({
  bucket: "nebutra-uploads",
  key: `${tenantId}/docs/report.pdf`,
  contentType: "application/pdf",
  tenantId,
  expiresIn: 3600, // seconds
});

// Client uploads directly to the storage provider
// PUT url  (with headers)

// Retrieve a signed download URL
const downloadUrl = await uploads.getSignedUrl("nebutra-uploads", key);

// Delete a file
await uploads.deleteFile("nebutra-uploads", key);

Environment variables

VariableRequiredDescription
STORAGE_PROVIDERNoExplicit provider: "s3" | "r2" | "s3-compatible"
AWS_BUCKET_NAMES3 onlyDefault bucket name
AWS_REGIONS3 onlyAWS region, e.g. us-east-1
AWS_ACCESS_KEY_IDS3 onlyIAM access key
AWS_SECRET_ACCESS_KEYS3 onlyIAM secret key
R2_BUCKET_NAMER2 onlyCloudflare R2 bucket name
R2_ACCOUNT_IDR2 onlyCloudflare account ID
R2_ACCESS_KEY_IDR2 onlyR2 API token ID
R2_SECRET_ACCESS_KEYR2 onlyR2 API token secret
R2_PUBLIC_URLR2 onlyPublic CDN base URL for R2

Provider auto-detection follows this priority:

  1. STORAGE_PROVIDER env var (if set)
  2. AWS_BUCKET_NAME present → s3
  3. R2_ACCOUNT_ID present → r2
  4. Fallback → in-memory (development only)

How is this guide?

Edit on GitHub

Last updated on

On this page