Article

Best Object Storage and File Upload Stack for Node.js SaaS Apps in 2026

Compare AWS S3, Cloudflare R2, DigitalOcean Spaces, Supabase Storage, UploadThing, and Filestack for Node.js SaaS file uploads, pricing, security, and delivery.

Introduction

Node.js SaaS apps rarely fail because they cannot store a file. They fail because file uploads become slow, expensive, insecure, or operationally messy after the product starts growing.

A production file stack has to handle avatars, PDF exports, customer attachments, invoices, CSV imports, private documents, user-generated media, background processing, malware checks, download permissions, CDN delivery, lifecycle rules, and cost monitoring. The storage bucket is only one part of the design.

This guide compares the main object storage and file upload options for Node.js SaaS applications in 2026: AWS S3, Cloudflare R2, DigitalOcean Spaces, Supabase Storage, UploadThing, and Filestack. It focuses on architecture, cost factors, security, and practical selection criteria rather than synthetic benchmark claims.

What a Production Node.js File Stack Actually Needs

A production SaaS file stack is more than multer plus a bucket. At minimum, it needs these layers:

  1. Upload authorization — Confirm that the current user can upload the file type and size.
  2. Upload transport — Avoid routing large files through your Node.js server when direct upload is safer and cheaper.
  3. Object storage — Store files durably with predictable access controls.
  4. Metadata storage — Track ownership, MIME type, size, checksum, processing status, and retention rules in PostgreSQL or another database.
  5. Processing pipeline — Generate thumbnails, scan files, extract metadata, or convert documents asynchronously.
  6. Delivery layer — Use signed URLs, public CDN URLs, or authenticated download endpoints depending on file privacy.
  7. Lifecycle management — Delete temporary uploads, expire abandoned files, and move old files to cheaper storage classes.
  8. Observability — Track upload failures, request volume, egress, transformation costs, and suspicious access patterns.

For most SaaS products, the right architecture is not “upload to the Node.js server, then copy to storage.” A better default is client direct upload using a short-lived presigned URL, followed by a server-side metadata record and optional background processing.

The AWS SDK for JavaScript supports generating presigned URLs for S3 using the modular v3 SDK. Here is a minimal example:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "us-east-1" });

async function generateUploadUrl(
  bucket: string,
  key: string,
  contentType: string
): Promise<string> {
  const command = new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    ContentType: contentType,
  });
  return getSignedUrl(s3, command, { expiresIn: 300 }); // 5 minutes
}

The same pattern applies to many S3-compatible providers by changing the endpoint configuration.

Quick Comparison Table

ProviderBest forPricing ModelNode.js FitMain Caution
AWS S3Enterprise SaaS, compliance-heavy apps, AWS-native stacksStorage, requests, retrieval, transfer, replication, accelerationExcellent SDK and ecosystemCost model has many components
Cloudflare R2Bandwidth-heavy asset delivery, Cloudflare-native apps, S3 alternativesStorage plus Class A/B operations; no egress bandwidth chargesGood S3-compatible optionRequest-heavy apps still need cost modeling
DigitalOcean SpacesSimple SaaS apps already on DigitalOceanFixed base subscription, included storage, included outbound transferSimple and practicalLess enterprise depth than AWS
Supabase StorageApps already using Supabase Auth/PostgresIncluded quota plus storage overageConvenient full-stack optionBest when you already use Supabase
UploadThingFast developer experience, full-stack app uploadsApp tiers and usage-based plansStrong DX for modern web appsLess control than raw object storage
FilestackUpload UI, transformations, file integrationsUploads, bandwidth, transformations, storage overagesGood API-first file platformCan be expensive for raw storage use cases

The baseline production architecture follows this data flow:

Browser / Mobile Client


Node.js API ── authenticate user, validate upload intent


Object Storage ── upload with presigned URL or managed upload API


Database ── create or update file metadata record


Queue ── process file asynchronously (thumbnails, scanning, conversion)


CDN or Signed Download URL ── deliver file to authorized users


Monitoring ── track failures, costs, and suspicious access

For small public assets such as avatars or product thumbnails, direct upload plus CDN delivery is usually enough. For private files such as invoices, contracts, customer documents, or exports, keep the object private and generate signed read URLs after checking application-level permissions.

For large videos, ZIP archives, or bulk imports, use multipart uploads and background processing. Do not keep a Node.js request open while scanning or transforming a large file. Store the upload metadata first, mark the record as processing, and update it asynchronously after workers finish.

// Example: issuing a presigned upload URL through a route handler
import { Router } from "express";
import { generateUploadUrl } from "./storage";
import { validateUploadIntent } from "./uploads";
import { db } from "./db";

const router = Router();

router.post("/uploads/presigned-url", async (req, res) => {
  const { fileName, contentType, fileSize } = req.body;

  // 1. Validate user intent
  const intent = await validateUploadIntent({
    userId: req.user.id,
    workspaceId: req.workspace.id,
    fileName,
    contentType,
    fileSize,
  });

  if (!intent.allowed) {
    return res.status(403).json({ error: intent.reason });
  }

  // 2. Generate presigned URL
  const key = `uploads/${req.workspace.id}/${intent.fileId}/${fileName}`;
  const uploadUrl = await generateUploadUrl(
    process.env.S3_BUCKET!,
    key,
    contentType
  );

  // 3. Create metadata record (pending)
  await db.file.create({
    id: intent.fileId,
    workspaceId: req.workspace.id,
    userId: req.user.id,
    key,
    fileName,
    contentType,
    fileSize,
    status: "pending_upload",
  });

  res.json({ uploadUrl, fileId: intent.fileId, key });
});

AWS S3: Safest Default for Mature Production Teams

AWS S3 remains the most mature default for SaaS teams that want deep ecosystem support, strong IAM controls, lifecycle policies, replication, storage classes, and integration with the broader AWS platform. AWS describes S3 pricing as pay-as-you-use with components including storage, request and retrieval pricing, data transfer, acceleration, management features, replication, transform, and query features.

That pricing model is powerful but also easy to underestimate. A simple app that stores a few GB of private documents may pay very little. A public media-heavy app with large outbound transfer, many small object reads, replication, and transformations may see a very different bill.

When to Choose S3

  • Your app already runs on AWS.
  • You need mature IAM, KMS, lifecycle rules, replication, auditability, or enterprise procurement.
  • Your team is comfortable modeling storage class, request, transfer, and replication costs.
  • You want maximum compatibility with SDKs, infrastructure-as-code tools, and third-party services.

Avoid making S3 the automatic answer when your main use case is simple public asset delivery and you are very sensitive to egress cost.

Cloudflare R2: Strong for Egress-Sensitive SaaS Assets

Cloudflare R2 is a compelling S3-compatible option for SaaS apps that care about bandwidth costs and already use Cloudflare for DNS, CDN, Workers, or edge security. Cloudflare’s R2 documentation states that pricing is based on storage plus two classes of operations, with Class A generally mutating state and Class B generally reading existing state. There are no egress bandwidth charges for any storage class.

That does not mean every R2 workload is free to serve. Request-heavy workloads still generate operation costs. Cloudflare’s own asset-hosting billing example shows how a workload with many daily reads can produce meaningful Class B operation charges even when storage is small.

When to Choose R2

  • You serve many downloads or public assets and want to reduce egress exposure.
  • You already use Cloudflare CDN, Workers, Pages, or WAF.
  • You want S3-compatible storage without the full AWS cost surface.
  • You are comfortable modeling request classes rather than only GB stored.

R2 is especially attractive for SaaS apps that serve generated files, exported reports, user assets, static downloads, or product media through Cloudflare.

// R2 uses the same S3 SDK — just change the endpoint
import { S3Client } from "@aws-sdk/client-s3";

const r2 = new S3Client({
  region: "auto",
  endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
});

DigitalOcean Spaces: Simple and Predictable for Smaller SaaS Teams

DigitalOcean Spaces is a practical option for teams that want simple S3-compatible object storage without committing to the broader complexity of AWS. Spaces starts at $5 per month for 250 GiB of storage with 1 TiB of outbound transfer included. Additional storage and outbound transfer are billed separately after the included quota.

This pricing shape is easy to understand for early-stage SaaS projects. It is less granular than S3 and may not have the same enterprise controls, but it can be much easier to explain and operate.

When to Choose Spaces

  • Your app already runs on DigitalOcean Droplets, App Platform, or Managed Databases.
  • You want a simple fixed starting point.
  • You need S3-compatible APIs without AWS-level operational complexity.
  • Your file volume is modest and predictable.

Spaces is a good fit for dashboards, B2B SaaS attachments, admin exports, small media libraries, and internal customer documents.

Supabase Storage: Convenient When Supabase Is Already Your Backend

Supabase Storage is best considered as part of the larger Supabase platform rather than as a standalone raw object storage competitor. If your Node.js or full-stack app already uses Supabase Auth, Postgres, Row Level Security, and Edge Functions, Supabase Storage can reduce integration work.

Supabase charges for the total size of assets in buckets, with overage pricing shown as GB-hour and GB-month values beyond plan quota across Free, Pro, Team, and Enterprise plans.

When to Choose Supabase Storage

  • You already use Supabase for Auth and Postgres.
  • You want storage permissions to live close to user and database policies.
  • You prefer integrated product velocity over raw infrastructure flexibility.
  • Your team values a single platform for early SaaS development.

Be cautious if storage is your dominant workload and you expect very high public delivery volume. In that case, compare Supabase Storage with R2, S3, or a CDN-backed object storage stack before committing.

// Supabase Storage upload example
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

async function uploadToSupabase(
  bucket: string,
  path: string,
  file: Buffer,
  contentType: string
) {
  const { data, error } = await supabase.storage
    .from(bucket)
    .upload(path, file, {
      contentType,
      upsert: false,
    });

  if (error) throw error;
  return data;
}

UploadThing: Developer Experience for Modern App Uploads

UploadThing is not just raw storage. It is a developer-focused file upload layer that helps teams implement uploads without building the entire presigned URL, validation, routing, and file handling flow from scratch. Its docs explain client-side uploads as a common approach where the server generates presigned URLs so the client can upload binary data without sending it through the application server.

UploadThing’s public pricing page lists a free 2 GB app, a 100 GB app plan, and a usage-based plan with included storage and overage. Confirm plan details before publishing because developer tooling pricing changes more often than cloud commodity pricing.

When to Choose UploadThing

  • You want to ship file uploads quickly.
  • Your team is using a modern full-stack framework.
  • You want upload routes, validation, private files, and developer-friendly abstractions.
  • The app’s upload workload is important but not yet large enough to justify a fully custom storage pipeline.

For a production SaaS product, UploadThing can be a good middle ground between “write everything yourself” and “buy a full enterprise file platform.”

Filestack: Managed Upload API and Transformation Platform

Filestack is more of a file platform than a simple storage bucket. It focuses on file picker UI, uploads, transformations, integrations, and delivery workflows. Its pricing page lists overage categories for bandwidth, uploads, transformations, and Filestack storage, which makes the cost model different from raw object storage.

When to Choose Filestack

  • You need a polished upload widget and file picker.
  • You need transformations, document handling, or integrations.
  • You prefer API-level file workflows over maintaining your own processing pipeline.
  • Your product value depends on file handling quality more than lowest storage cost.

Do not choose Filestack only to store cheap bytes. Choose it when the upload and transformation experience saves meaningful engineering time.

Cost Factors Most Teams Underestimate

Object storage cost is rarely just “GB stored.” Review these dimensions before choosing a provider:

Cost DimensionWhat to Watch
StorageGB-month pricing and minimum retention periods
RequestsPUT, LIST, GET, multipart, lifecycle transitions, and metadata operations
EgressDownloads to users, cross-region transfer, CDN origin pulls, and provider-specific bandwidth rules
TransformationsImage resizing, video previews, document conversion, OCR, and malware scanning
ReplicationCross-region copies, disaster recovery, and multi-region access
CDNCache hit rate, invalidation, signed delivery, and origin shielding
Temporary filesAbandoned uploads, expired exports, and unreferenced objects
Support and complianceAudit logs, retention policies, enterprise support, and data residency

For SaaS products with many small files, request cost can matter more than storage. For media-heavy products, egress and transformation costs often dominate. For B2B document apps, private access and auditability may matter more than the cheapest GB-month price.

Security Checklist for Node.js File Uploads

A production upload flow should apply these controls:

  1. Use short-lived upload URLs. Do not expose long-lived credentials to browsers.
  2. Validate user intent before upload. Check account, workspace, plan limits, file type, and size before issuing an upload URL.
  3. Keep private files private by default. Use signed read URLs or authenticated download endpoints.
  4. Store metadata in your database. The bucket should not be your source of truth for ownership or permissions.
  5. Verify MIME type and extension carefully. Do not trust only client-provided metadata.
  6. Scan risky uploads asynchronously. Use a queue and mark files as unavailable until approved if your risk profile requires it.
  7. Limit upload size by product plan. Connect file limits to billing and quota rules.
  8. Use lifecycle rules. Delete expired exports, failed uploads, and abandoned temporary objects.
  9. Log important events. Track upload creation, download authorization, deletion, and permission failures.
  10. Review public bucket policy. Accidental public exposure is more expensive than a slightly more complex signed URL flow.
// Example: validating MIME type with file-type (checks magic bytes, not just extension)
import { fileTypeFromBuffer } from "file-type";

async function validateFileType(
  buffer: Buffer,
  allowedTypes: string[]
): Promise<boolean> {
  const type = await fileTypeFromBuffer(buffer);
  if (!type) return false;
  return allowedTypes.includes(type.mime);
}

Which Provider Should You Choose?

  • Choose AWS S3 if your SaaS is enterprise-facing, AWS-native, compliance-sensitive, or likely to need advanced storage controls.
  • Choose Cloudflare R2 if you expect heavy downloads, public asset delivery, or Cloudflare-native deployment, and you want to reduce egress complexity.
  • Choose DigitalOcean Spaces if you want simple S3-compatible storage with predictable starter pricing and your infrastructure already sits on DigitalOcean.
  • Choose Supabase Storage if Supabase is already your application backend and you want storage integrated with your auth and database model.
  • Choose UploadThing if you want the fastest path to clean app-level uploads without writing the full pipeline yourself.
  • Choose Filestack if file uploading, picking, transformations, and managed file workflows are product-critical enough to justify platform pricing.

A reasonable default stack for many Node.js SaaS apps is:

  • Node.js API + PostgreSQL metadata
  • Client direct upload with presigned URLs
  • S3-compatible object storage
  • Private-by-default bucket policy
  • Queue-based processing for thumbnails/scanning
  • CDN or signed read URLs for delivery
  • Monitoring on errors, request volume, and storage cost

Conclusion

The best object storage choice for a Node.js SaaS app depends less on the logo and more on the workload.

For enterprise depth, choose AWS S3. For bandwidth-heavy public delivery, evaluate Cloudflare R2. For simple S3-compatible storage, DigitalOcean Spaces is easy to reason about. For Supabase-native apps, Supabase Storage can reduce integration work. For fast upload UX, UploadThing and Filestack can save engineering time.

Before publishing your final stack, model at least three scenarios: a small private-document SaaS, a media-heavy public asset app, and a high-request workload with many small files. That exercise will reveal whether storage, requests, egress, transformations, or operational complexity is the real constraint.

FAQ

Is AWS S3 still the safest default for Node.js SaaS file uploads?
Yes, for many production teams. S3 has the deepest ecosystem, strong IAM controls, mature SDKs, storage classes, lifecycle rules, and enterprise adoption. The tradeoff is pricing complexity. If your app is bandwidth-heavy or simple enough that you do not need the full AWS ecosystem, compare S3 with Cloudflare R2, DigitalOcean Spaces, or another S3-compatible option.
Should a Node.js SaaS app upload files through the server or directly to object storage?
Most production apps should use direct client uploads with short-lived presigned URLs. The Node.js server should authenticate the user, validate upload intent, issue the upload URL, and store metadata. It should not usually stream every large binary payload through the API server unless you have a specific processing or compliance reason.
When should I use UploadThing or Filestack instead of raw object storage?
Use an upload API when developer speed, file picker UI, validation workflow, transformations, and managed file handling are more valuable than lowest raw storage cost. Use raw S3-compatible storage when you need maximum control, lowest commodity cost, or custom infrastructure patterns.