副项目墓场堆满了技术 impressive 的失败案例
大多数试图将副项目转变为产品的开发者都会犯同样的错误:他们在技术决策上过度工程化,而在产品决策上却工程不足。副项目拥有优美的架构、完善的测试覆盖率和精良的 CI/CD 管道——但却没有用户。这不是技术失败,而是优先级排序失败。本指南涵盖了从副项目转变为产品时需要关注的具体技术决策,以及同样重要的是,那些你认为重要但实际上可以推迟很久才做的决策。
转变时刻
当除了你之外有人依赖你的副项目时,它就成为了产品。不是当你有 100 个用户时,不是当你在 Product Hunt 上发布时。当哪怕只有一个真实用户会注意到服务宕机时,那就是风险改变、技术决策需要反映不同优先级的时刻。
在那之前,优化迭代速度。在那之后,你需要平衡迭代速度与可靠性、可观测性以及让他人能够上手代码库的能力。这些是不同的优化目标,而服务于一个目标的架构决策往往会损害另一个目标。
决策 1:部署平台
你的部署平台选择会限制你的架构,影响你在规模化时的成本,并决定你在基础设施与功能开发上花费多少运营时间。早期要深思熟虑地选择,因为后期迁移会很痛苦。
选择范围
Vercel/Render/Railway(托管 PaaS): 零基础设施管理。通过 git push 部署。空闲时扩展至零。如果你是独立开发者,希望在产生收入前不花时间在基础设施上,直到有足够的理由聘请专职的 DevOps,这是正确的选择。随着规模扩大,经济性会变差——Vercel 在大量流量下的定价对独立开发者不友好——但在早期阶段,节省的时间是真实且显著的。
单个 VPS(Hetzner、DigitalOcean、Fly.io): 一个每月 20 美元的 VPS,配合 Caddy、Docker Compose 和 PostgreSQL,可以处理相当多的生产流量。这是我们”$50/月服务器”文章中描述的架构:你拥有服务器,控制配置,成本可预测。权衡之处在于每个基础设施问题都是你的问题——但大多数问题一个下午就能解决,一旦修复就很少复发。
AWS/GCP/Azure (云服务提供商): 当你有只有它们才能满足的特殊需求时,这些是正确的选择:合规认证(SOC 2, HIPAA)、特定的地理可用性要求,或与企业客户环境的紧密集成。如果你是独立开发者或小团队,不要从这里开始。运营复杂性和计费不可预测性是对你注意力的昂贵消耗。
# Docker Compose 生产部署 — 适合 0 至 1 万美元月度经常性收入(MRR)
# 在单个 Hetzner CX31 (4 vCPU, 8GB RAM, $15/月)上运行
version: '3.8'
services:
app:
image: ghcr.io/yourorg/yourapp:${APP_VERSION:-latest}
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://app:${DB_PASSWORD}@postgres:5432/appdb
- REDIS_URL=redis://redis:6379
- SECRET_KEY=${SECRET_KEY}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
labels:
- "caddy=app.example.com"
- "caddy.reverse_proxy=:3000"
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
caddy:
image: lucaslorentz/caddy-docker-proxy:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- caddy_data:/data
volumes:
postgres_data:
redis_data:
caddy_data:
决策 2:单体架构 vs. 微服务
从单体架构开始。这在 2026 年已不是有争议的观点——即使是微服务的倡导者(包括普及这一术语的 ThoughtWorks 团队)也已发表了大量关于产品在没有既定扩展需求时应采用单体架构优先方法的文章。
实际原因:单体架构让你能低成本地重构边界。当你发现(而且你一定会发现)你错误地划定了领域边界时,一次函数调用只需变成另一次函数调用。在微服务架构中,同样的重构涉及重写服务契约、更新 API 客户端和协调部署。错误边界的成本要高得多。
当特定组件的扩展特性与应用程序其余部分显著不同时,当组件因组织原因需要独立部署时,或者当团队规模扩大到两个团队共同拥有同一代码库会产生协调开销时,应提取服务。在您获得有意义的收入和超过三到五名工程师的团队规模之前,这些条件通常都不适用。
决策3:从一开始就进行身份验证
身份验证是后期更改成本最高的技术决策。在启动时自行构建身份验证系统以避免身份验证提供商的复杂性,然后在拥有真实用户时迁移到适当的身份验证系统,这是一段痛苦的经历。您的用户会话将失效,并且您将花费数天时间测试密码重置流程中的边缘情况。
从一开始就使用身份验证库或服务。2026年的选择:
- Clerk: 最面向开发者的托管身份验证服务。预构建的UI组件、用户事件webhook、组织管理。每月25美元,最多支持10,000名月活跃用户。适合大多数B2B SaaS产品。
- Auth.js(前身为NextAuth): 开源,在您的应用程序中运行,与您自己的数据库集成。无每用户成本。设置更多,但完全控制。最适合希望了解内部运作情况且不依赖第三方身份验证提供商的开发者。
- Lucia Auth: 用于TypeScript的轻量级身份验证库,与数据库无关。比Auth.js更有主见——它干净地处理会话管理,并将UI完全留给您。
# Auth.js v5配置(框架无关)
// auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "@/lib/db"
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
],
session: {
strategy: "database",
maxAge: 30 * 24 * 60 * 60, // 30天
},
callbacks: {
async session({ session, user }) {
// 将附加用户数据附加到会话
session.user.id = user.id
session.user.plan = user.plan ?? "free"
return session
},
},
})
决策4:数据库架构是您早期最昂贵的错误
随着用户数据的积累,更改数据库架构的成本越来越高。并非不可能——零停机迁移策略(添加列、回填、部署、删除旧列)可以处理大多数架构变更——但在时间和测试开销方面成本很高。
最昂贵的早期数据库错误是软删除记录处理(你是否忘记了在查询中过滤 WHERE deleted_at IS NULL?)、多租户隔离(行级隔离与每租户一个数据库)以及时间戳时区假设。
-- 经久耐用的基础表结构
-- 从一开始就为每个表遵循这种模式
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- 多租户:所有用户数据都限定在组织范围内
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
-- 业务字段
name TEXT NOT NULL,
slug TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'active', 'archived')),
metadata JSONB NOT NULL DEFAULT '{}',
-- 审计字段:始终包含这些
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
-- 软删除:要么从第一天就包含,要么完全不要
deleted_at TIMESTAMPTZ,
UNIQUE (org_id, slug)
);
-- 行级安全:在数据库级别强制执行组织隔离
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY products_org_isolation ON products
USING (org_id = current_setting('app.current_org_id')::uuid);
-- 在任何行更改时自动更新 updated_at
CREATE TRIGGER products_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
决策5:在需要之前建立可观测性
最困难的调试会发生在没有可观测性的生产系统上。在拥有用户之前设置结构化日志记录和基本指标只需两小时,却能节省数十小时的”我想知道为什么会发生”的调试时间。
# 使用 Pino 在 Node.js 中进行结构化日志记录(快速,JSON 输出)
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
// 在来自请求的所有日志中包含请求上下文
mixin: () => ({
environment: process.env.NODE_ENV,
version: process.env.APP_VERSION,
}),
})
// 在你的请求处理器中:
export async function handleRequest(req, res) {
const requestLogger = logger.child({
requestId: req.headers['x-request-id'] ?? crypto.randomUUID(),
userId: req.user?.id,
path: req.path,
method: req.method,
})
try {
requestLogger.info('request_started')
const result = await processRequest(req)
requestLogger.info({ statusCode: 200 }, 'request_completed')
return res.json(result)
} catch (error) {
requestLogger.error({
error: error.message,
stack: error.stack,
statusCode: 500
}, 'request_failed')
return res.status(500).json({ error: 'Internal server error' })
}
}
目前尚不重要的决策
在通过付费客户验证产品市场契合度之前,不要在这些方面花费时间:
- 微服务、服务边界、事件溯源架构
- 多区域部署和全球低延迟需求
- 水平自动扩展和 Kubernetes
- 自定义后台任务基础设施(一个简单的基于数据库的队列可以满足 99% 的需求)
- 只读副本(PostgreSQL 处理的并发读取量比大多数副项目生成的要多)
这些是在大规模时才真正需要关注的问题。在达到规模之前,它们只会分散注意力。在副项目中过早优化的成本不是性能开销——而是你花在基础设施上而不是与用户交流和发布功能上的数周工程时间。
关键要点
- 对于大多数从零到产生有意义收入的产品,一台使用 Docker Compose 和 PostgreSQL 的 VPS 就足够了。在有了证明其合理性的扩展需求之前,不要增加复杂性。
- 从单体架构开始。当你有特定的、已证明的理由时才提取服务——而不是基于架构理想。
- 从一开始就使用成熟的认证库或托管认证服务。认证是后期迁移最痛苦的系统。
- 从一开始就构建具有多租户隔离、软删除(如果需要)和时区感知时间戳的数据库模式。
- 在拥有用户之前就添加结构化日志,而不是在第一次神秘的生产事故之后。
