引言
Node.js SaaS 应用很少会因为无法存储文件而失败。它们失败的原因往往是:在产品开始增长后,文件上传变得缓慢、昂贵、不安全,或者运营上变得混乱不堪。
一个生产环境的文件技术栈需要处理以下内容:头像、PDF 导出、客户附件、发票、CSV 导入、私有文档、用户生成媒体、后台处理、恶意软件扫描、下载权限、CDN 交付、生命周期规则以及成本监控。存储桶只是整个设计中的一环。
本指南比较了 2026 年面向 Node.js SaaS 应用的主要对象存储和文件上传选项:AWS S3、Cloudflare R2、DigitalOcean Spaces、Supabase Storage、UploadThing 以及 Filestack。它更侧重于架构、成本因素、安全性和实际的选择标准,而非人为的基准测试声明。
生产环境 Node.js 文件栈实际需要什么
一个生产环境的 SaaS 文件栈远不止是 multer 加一个存储桶。它至少需要以下几个层次:
- 上传授权 — 确认当前用户可以上传特定类型和大小的文件。
- 上传传输 — 当直接上传更安全且成本更低时,避免让大文件流经你的 Node.js 服务器。
- 对象存储 — 以可预期的访问控制持久化存储文件。
- 元数据存储 — 在 PostgreSQL 或其他数据库中跟踪所有者、MIME 类型、大小、校验和、处理状态以及保留规则。
- 处理管道 — 异步生成缩略图、扫描文件、提取元数据或转换文档。
- 交付层 — 根据文件的隐私性,使用签名 URL、公共 CDN URL 或受认证保护的下载端点。
- 生命周期管理 — 删除临时上传、废弃文件,并将旧文件移动到更便宜的存储类。
- 可观测性 — 跟踪上传失败、请求量、出口流量、转换成本和可疑的访问模式。
对于大多数 SaaS 产品来说,正确的架构不是“先上传到 Node.js 服务器,再拷贝到存储”。一个更好的默认方案是客户端使用短期有效预签名 URL 直接上传,随后在服务器端记录元数据,并可选地进行后台处理。
AWS JavaScript SDK 支持使用模块化的 v3 SDK 为 S3 生成预签名 URL。下面是一个最小示例:
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分钟
}
同样的模式也适用于许多 S3 兼容的提供商,只需修改端点配置即可。
快速对比表
| 提供商 | 最适合 | 定价模式 | Node.js 适配性 | 主要注意事项 |
|---|---|---|---|---|
| AWS S3 | 企业 SaaS、合规要求高的应用、AWS 原生技术栈 | 存储、请求、检索、传输、复制、加速 | 优秀的 SDK 和生态系统 | 成本模型包含很多组成部分 |
| Cloudflare R2 | 带宽密集型资产交付、Cloudflare 原生应用、S3 替代方案 | 存储加 A/B 类操作;无出口带宽费用 | 良好的 S3 兼容选项 | 请求密集型应用仍需成本建模 |
| DigitalOcean Spaces | 已在使用 DigitalOcean 的简单 SaaS 应用 | 固定基础订阅,包含存储和出站传输量 | 简单实用 | 企业级深度不及 AWS |
| Supabase Storage | 已使用 Supabase Auth/Postgres 的应用 | 包含配额加存储超额费用 | 便利的全栈选项 | 最适合你已在使用 Supabase 的情况 |
| UploadThing | 快速开发者体验、全栈应用上传 | 应用套餐和按用量计费计划 | 对现代 Web 应用有极佳的 DX | 控制力弱于原始对象存储 |
| Filestack | 上传 UI、文件转换和文件集成 | 上传、带宽、转换、存储超额 | 良好的 API 优先文件平台 | 对于原始存储用例可能较昂贵 |
推荐的 Node.js SaaS 上传架构
生产环境的基线架构遵循以下数据流:
浏览器/移动客户端
│
▼
Node.js API ── 认证用户,验证上传意图
│
▼
对象存储 ── 通过预签名 URL 或托管上传 API 进行上传
│
▼
数据库 ── 创建或更新文件元数据记录
│
▼
队列 ── 异步处理文件(缩略图、扫描、转换)
│
▼
CDN 或签名下载 URL ── 向授权用户交付文件
│
▼
监控 ── 跟踪失败、成本和可疑访问
对于头像或产品缩略图这类小型公共资产,直接上传加 CDN 交付通常就足够了。对于发票、合同、客户文档或导出文件等私密文件,应保持对象私有,并在检查应用级权限后生成签名读取 URL。
对于大型视频、ZIP 归档或批量导入,应使用分片上传和后台处理。不要在扫描或转换大文件时让 Node.js 请求一直保持打开状态。先存储上传元数据,将记录标记为 processing,然后在工作线程完成后异步更新。
// 示例:通过路由处理程序签发预签名上传 URL
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. 验证用户意图
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. 生成预签名 URL
const key = `uploads/${req.workspace.id}/${intent.fileId}/${fileName}`;
const uploadUrl = await generateUploadUrl(
process.env.S3_BUCKET!,
key,
contentType
);
// 3. 创建待上传的元数据记录
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:成熟生产团队最安全的默认选择
对于那些希望获得深厚生态系统支持、强大的 IAM 控制、生命周期策略、复制功能、存储类以及与更广泛 AWS 平台集成的 SaaS 团队来说,AWS S3 仍然是最成熟的默认选择。AWS 将 S3 定价描述为按使用量付费,包含存储、请求和检索定价、数据传输、加速、管理功能、复制、转换和查询等组成部分。
这种定价模型功能强大,但也容易被低估。一个存储几 GB 私有文档的简单应用可能只需支付很少的费用。而一个公共媒体密集型应用,如果有大量出站传输、许多小对象读取、复制和转换,账单可能会大不相同。
选择 S3 的时机
- 你的应用已在 AWS 上运行。
- 你需要成熟的 IAM、KMS、生命周期规则、复制、可审计性或企业采购。
- 你的团队擅长对存储类、请求、传输和复制成本进行建模。
- 你希望与 SDK、基础设施即代码工具以及第三方服务有最大兼容性。
当你的主要用例是简单的公共资产交付,并且你对出口成本非常敏感时,不要自动选择 S3 作为答案。
Cloudflare R2:对出口敏感的 SaaS 资产优势明显
对于关心带宽成本且已使用 Cloudflare 进行 DNS、CDN、Workers 或边缘安全的 SaaS 应用,Cloudflare R2 是一个极具吸引力的 S3 兼容选项。Cloudflare 的 R2 文档指出,定价基于存储加上两类操作,A 类通常用于变更状态,B 类通常用于读取现有状态。所有存储类均无出口带宽费用。
但这并不意味着每个 R2 工作负载的服务都是免费的。请求密集型工作负载仍然会产生操作成本。Cloudflare 自己的资产托管计费示例显示,即使存储量很小,每日大量读取的工作负载也可能产生可观的 B 类操作费用。
选择 R2 的时机
- 你提供大量下载或公共资产,并希望降低出口风险。
- 你已在使用 Cloudflare 的 CDN、Workers、Pages 或 WAF。
- 你想要 S3 兼容的存储,但不希望面对完整的 AWS 成本面。
- 你更擅长对请求类别建模,而不仅仅是按存储的 GB 费用。
对于通过 Cloudflare 提供生成文件、导出报告、用户资产、静态下载或产品媒体的 SaaS 应用,R2 尤其有吸引力。
// R2 使用相同的 S3 SDK——只需更改端点
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:适合小型 SaaS 团队的简单且可预测的选择
对于希望获得简单的 S3 兼容对象存储而无需承担 AWS 整体复杂性的团队,DigitalOcean Spaces 是一个实用的选项。Spaces 起价为每月 5 美元,包含 250 GiB 存储和 1 TiB 出站传输。超出包含配额后,额外存储和出站传输将单独计费。
这种定价形态对早期 SaaS 项目来说很容易理解。它不像 S3 那样精细,可能不具备相同级别的企业控制力,但解释和操作起来都要容易得多。
选择 Spaces 的时机
- 你的应用已在 DigitalOcean Droplets、App Platform 或托管数据库上运行。
- 你想要一个简单的固定起点。
- 你需要 S3 兼容的 API,但不想面对 AWS 级别的运维复杂性。
- 你的文件量适中且可预测。
对于仪表盘、B2B SaaS 附件、管理导出、小型媒体库和内部客户文档,Spaces 是一个不错的选择。
Supabase Storage:当 Supabase 已是你的后端时很便利
最好将 Supabase Storage 视为更大的 Supabase 平台的一部分,而不是一个独立的原始对象存储竞品。如果你的 Node.js 或全栈应用已在使用 Supabase Auth、Postgres、行级安全策略和 Edge Functions,那么 Supabase Storage 可以减少集成工作。
Supabase 根据存储桶中资产的总大小收费,超额费用按 GB-小时和 GB-月计算,超出免费、Pro、Team 和 Enterprise 计划配额的资源。
选择 Supabase Storage 的时机
- 你已在使用 Supabase 进行认证和 Postgres。
- 你希望存储权限与用户和数据库策略紧密结合。
- 你更看重集成产品的开发速度,而非原始基础设施的灵活性。
- 你的团队在早期 SaaS 开发中看重单一平台。
如果存储是你的主要工作负载,并且你预期有非常高的公共交付量,则需谨慎。在这种情况下,在确定之前,请将 Supabase Storage 与 R2、S3 或 CDN 支持的对象存储栈进行比较。
// Supabase Storage 上传示例
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:现代应用上传的开发者体验
UploadThing 不仅仅是原始存储。它是一个面向开发者的文件上传层,帮助团队实现上传功能,而无需从头构建完整的预签名 URL、验证、路由和文件处理流程。它的文档将客户端上传解释为一种常见方法,即服务器生成预签名 URL,以便客户端可以直接上传二进制数据而无需经过应用服务器。
UploadThing 的公开定价页面列出了免费 2 GB 应用、100 GB 应用计划以及包含存储和超额用量的按量付费计划。在发布前请确认计划详情,因为开发者工具的定价变化比云商品定价更频繁。
选择 UploadThing 的时机
- 你希望快速交付文件上传功能。
- 你的团队正在使用现代全栈框架。
- 你想要上传路由、验证、私有文件以及开发者友好的抽象。
- 应用的上传工作负载很重要,但还没有庞大到需要完全自定义存储管道的程度。
对于生产环境的 SaaS 产品,UploadThing 可以成为“完全自己编写”和“购买完整企业文件平台”之间的一个良好中间点。
Filestack:托管上传 API 和转换平台
Filestack 更像一个文件平台,而非简单的存储桶。它专注于文件选择器 UI、上传、转换、集成和交付工作流程。其定价页面列出了带宽、上传、转换和 Filestack 存储的超额类别,这使得其成本模型与原始对象存储不同。
选择 Filestack 的时机
- 你需要一个精美的上传小部件和文件选择器。
- 你需要文件转换、文档处理或集成。
- 你更倾向于 API 级别的文件工作流程,而非维护自己的处理管道。
- 你的产品价值更依赖于文件处理质量,而非最低的存储成本。
不要仅仅为了存储廉价的字节而选择 Filestack。当上传和转换体验能节省大量工程时间时,再选择它。
大多数团队低估的成本因素
对象存储成本很少仅仅是“每 GB 存储费”。在选择提供商之前,请审查以下几个维度:
| 成本维度 | 注意事项 |
|---|---|
| 存储 | 每 GB·月定价和最短保留期 |
| 请求 | PUT、LIST、GET、分片上传、生命周期转换和元数据操作 |
| 出口 | 向用户下载、跨区域传输、CDN 回源拉取以及提供商特定的带宽规则 |
| 转换 | 图像缩放、视频预览、文档转换、OCR 和恶意软件扫描 |
| 复制 | 跨区域副本、灾难恢复和多区域访问 |
| CDN | 缓存命中率、失效操作、签名交付和源站防护 |
| 临时文件 | 废弃上传、过期导出和未引用的对象 |
| 支持和合规 | 审计日志、保留策略、企业支持和数据驻留 |
对于拥有大量小文件的 SaaS 产品,请求成本可能比存储成本更关键。对于媒体密集型产品,出口和转换成本通常占主导。对于 B2B 文档应用,私有访问和可审计性可能比最低的每 GB·月价格更重要。
Node.js 文件上传安全检查清单
生产环境的上传流程应实施以下控制:
- 使用短期上传 URL。 不要向浏览器暴露长期有效的凭证。
- 上传前验证用户意图。 在签发上传 URL 之前,检查账户、工作区、计划限制、文件类型和大小。
- 默认保持私有文件私有。 使用签名读取 URL 或受认证保护的下载端点。
- 在数据库中存储元数据。 存储桶不应成为你关于所有权或权限的单一事实来源。
- 仔细验证 MIME 类型和扩展名。 不要只信任客户端提供的元数据。
- 异步扫描有风险的上传文件。 使用队列;若风险状况需要,将文件标记为不可用,直至获得批准。
- 按产品计划限制上传大小。 将文件限制与计费和配额规则关联。
- 使用生命周期规则。 删除过期的导出成果、失败的上传和废弃的临时对象。
- 记录重要事件。 跟踪上传创建、下载授权、删除和权限失败。
- 审查公共存储桶策略。 意外的公开暴露比稍微复杂一些的签名 URL 流程代价更高。
// 示例:使用 file-type 验证 MIME 类型(检查魔数,而非仅扩展名)
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);
}
你应该选择哪个提供商?
- 如果你的 SaaS 面向企业、运行在 AWS 原生环境、对合规敏感或可能需要高级存储控制,请选择 AWS S3。
- 如果你预计会有大量下载、公共资产交付或 Cloudflare 原生部署,并希望降低出口复杂性,请选择 Cloudflare R2。
- 如果你想要简单的 S3 兼容存储、可预测的入门定价,并且基础设施已基于 DigitalOcean,请选择 DigitalOcean Spaces。
- 如果 Supabase 已是你的应用后端,并且你希望存储与你现有的认证和数据库模型集成,请选择 Supabase Storage。
- 如果你希望以最快的方式获得干净的应用级上传功能,而无需自行编写完整管道,请选择 UploadThing。
- 如果文件上传、选取、转换和托管文件工作流程对产品至关重要,足以证明平台定价的合理性,请选择 Filestack。
对于许多 Node.js SaaS 应用,一个合理的默认技术栈是:
- Node.js API + PostgreSQL 存储元数据
- 使用预签名 URL 进行客户端直接上传
- S3 兼容的对象存储
- 默认私有的存储桶策略
- 基于队列的缩略图/扫描处理
- 用于交付的 CDN 或签名读取 URL
- 对错误、请求量和存储成本的监控
结论
对于 Node.js SaaS 应用而言,最佳对象存储的选择更多地取决于工作负载,而非提供商的品牌。
要获得企业级深度,请选择 AWS S3。对于带宽密集型公共交付,请评估 Cloudflare R2。对于简单的 S3 兼容存储,DigitalOcean Spaces 易于推理。对于 Supabase 原生应用,Supabase Storage 可以减少集成工作。要实现快速的上传用户体验,UploadThing 和 Filestack 可以节省工程时间。
在最终确定技术栈之前,至少对三种场景进行建模:一个小型私有文档 SaaS、一个媒体密集型公共资产应用,以及一个包含大量小文件的高请求量工作负载。这种演练将揭示出存储、请求、出口、转换或是运营复杂性哪个是真正的制约因素。