Loading...
Loading...
Compare original and translation side by side
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.x |
| React | React | 19.x |
| Runtime | Bun | Latest |
| Database | PostgreSQL (Railway) | - |
| ORM | Prisma | 7.x |
| Auth | NextAuth v5 (Auth.js) | 5.0.0-beta |
| UI | shadcn/ui (new-york) | Latest |
| Styling | Tailwind CSS | v4 |
| Theming | next-themes | Latest |
| Icons | Lucide React | Latest |
| State | TanStack React Query | 5.x |
| Validation | Zod | 4.x |
| Resend | 6.x | |
| Storage | Railway S3-compatible | - |
| Jobs | pg-boss | 12.x |
| Hosting | Railway | - |
| 层级 | 技术栈 | 版本 |
|---|---|---|
| 框架 | Next.js (App Router) | 16.x |
| React | React | 19.x |
| 运行时 | Bun | Latest |
| 数据库 | PostgreSQL (Railway) | - |
| ORM | Prisma | 7.x |
| 身份验证 | NextAuth v5 (Auth.js) | 5.0.0-beta |
| UI组件库 | shadcn/ui (new-york) | Latest |
| 样式 | Tailwind CSS | v4 |
| 主题 | next-themes | Latest |
| 图标 | Lucide React | Latest |
| 状态管理 | TanStack React Query | 5.x |
| 校验 | Zod | 4.x |
| 邮件服务 | Resend | 6.x |
| 存储 | Railway S3-compatible | - |
| 任务队列 | pg-boss | 12.x |
| 托管平台 | Railway | - |
┌─────────────────────────────────────────────────────────────────────┐
│ Next.js Application │
├─────────────────────────────────────────────────────────────────────┤
│ Landing Page │ Auth Pages │ Dashboard (Protected) │
│ / │ /login │ /teams/[teamId]/* │
│ /pricing │ /register │ /dashboard │
│ │ /invite/[t] │ /settings │
├─────────────────────────────────────────────────────────────────────┤
│ API Routes │
│ /api/auth/* │ /api/teams/* │ /api/uploads/* │
│ /api/cron/* │ /api/webhooks/* │ /api/invitations/* │
├─────────────────────────────────────────────────────────────────────┤
│ proxy.ts (Security Headers) │ layout.tsx (Auth Protection) │
├─────────────────────────────────────────────────────────────────────┤
│ Services Layer │
│ Prisma (DB) │ Resend (Email) │ S3 (Storage) │ pg-boss (Jobs) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Railway Infrastructure │
│ PostgreSQL │ S3 Bucket │ Redis (optional) │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ Next.js Application │
├─────────────────────────────────────────────────────────────────────┤
│ Landing Page │ Auth Pages │ Dashboard (Protected) │
│ / │ /login │ /teams/[teamId]/* │
│ /pricing │ /register │ /dashboard │
│ │ /invite/[t] │ /settings │
├─────────────────────────────────────────────────────────────────────┤
│ API Routes │
│ /api/auth/* │ /api/teams/* │ /api/uploads/* │
│ /api/cron/* │ /api/webhooks/* │ /api/invitations/* │
├─────────────────────────────────────────────────────────────────────┤
│ proxy.ts (Security Headers) │ layout.tsx (Auth Protection) │
├─────────────────────────────────────────────────────────────────────┤
│ Services Layer │
│ Prisma (DB) │ Resend (Email) │ S3 (Storage) │ pg-boss (Jobs) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Railway Infrastructure │
│ PostgreSQL │ S3 Bucket │ Redis (optional) │
└─────────────────────────────────────────────────────────────────────┘User ─┬─> TeamMember ─> Team ─┬─> Creator
│ ├─> Campaign
└─> TeamMember ─> Team ├─> Content
└─> ... (all team resources)User ─┬─> TeamMember ─> Team ─┬─> Creator
│ ├─> Campaign
└─> TeamMember ─> Team ├─> Content
└─> ... (all team resources)src/app/
├── (auth)/ # Public auth pages (login, register)
│ └── layout.tsx # Client component, styling only
├── (dashboard)/ # Protected authenticated routes
│ ├── layout.tsx # Server component with auth() check + redirect
│ └── teams/[teamId]/ # Team-scoped pages
├── (marketing)/ # Public marketing pages
├── invite/[token]/ # Team invitation acceptance
└── api/ # API routessrc/app/
├── (auth)/ # 公开的身份验证页面(登录、注册)
│ └── layout.tsx # 客户端组件,仅负责样式
├── (dashboard)/ # 受保护的已验证路由
│ ├── layout.tsx # 服务端组件,包含auth()检查与重定向
│ └── teams/[teamId]/ # 团队范围的页面
├── (marketing)/ # 公开的营销页面
├── invite/[token]/ # 团队邀请接受页面
└── api/ # API路由// src/app/(dashboard)/layout.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({ children }) {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return <DashboardShell session={session}>{children}</DashboardShell>;
}// src/app/(dashboard)/layout.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({ children }) {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return <DashboardShell session={session}>{children}</DashboardShell>;
}// Use api-helpers for clean, consistent patterns
import { requireTeamMember, parseJsonBody, withErrorHandler, ApiError } from "@/lib/api-helpers";
type RouteParams = {
params: Promise<{ teamId: string }>; // Next.js 15+ async params
};
export const POST = withErrorHandler(async (req: NextRequest, { params }: RouteParams) => {
const { teamId } = await params; // MUST await params
const { userId, role } = await requireTeamMember(teamId); // Throws 401/403
if (role !== "ADMIN") {
throw new ApiError("Admin access required", 403);
}
const data = await parseJsonBody(req, mySchema); // Validates with Zod
// ... business logic
return NextResponse.json(result);
});// 使用api-helpers实现清晰、一致的模式
import { requireTeamMember, parseJsonBody, withErrorHandler, ApiError } from "@/lib/api-helpers";
type RouteParams = {
params: Promise<{ teamId: string }>; // Next.js 15+ 异步参数
};
export const POST = withErrorHandler(async (req: NextRequest, { params }: RouteParams) => {
const { teamId } = await params; // 必须await参数
const { userId, role } = await requireTeamMember(teamId); // 抛出401/403错误
if (role !== "ADMIN") {
throw new ApiError("Admin access required", 403);
}
const data = await parseJsonBody(req, mySchema); // 使用Zod校验
// ... 业务逻辑 ...
return NextResponse.json(result);
});src/
├── app/
│ ├── (auth)/ # Auth pages (public)
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx # Client component, no auth
│ ├── (dashboard)/ # Protected pages
│ │ ├── layout.tsx # Server component, auth check
│ │ ├── dashboard-shell.tsx # Sidebar + nav
│ │ └── teams/[teamId]/
│ ├── invite/[token]/page.tsx # Invitation acceptance
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── teams/
│ │ │ ├── route.ts # List/create teams
│ │ │ └── [teamId]/
│ │ │ ├── route.ts # Team CRUD
│ │ │ ├── members/
│ │ │ └── invitations/
│ │ ├── invitations/
│ │ │ └── [token]/accept/route.ts
│ │ └── uploads/[...path]/route.ts # S3 proxy
│ ├── globals.css # Design system
│ ├── layout.tsx # Root layout with Providers
│ └── page.tsx # Landing page
├── components/
│ ├── ui/ # shadcn components
│ ├── teams/ # Team management
│ ├── invitations/ # Invitation UI
│ ├── shared/ # Reusable patterns
│ ├── providers.tsx # App providers
│ └── theme-toggle.tsx # Theme switcher
├── hooks/
│ ├── use-teams.ts # Team hooks
│ └── ...
├── lib/
│ ├── auth.ts # NextAuth config (Node.js runtime)
│ ├── auth.config.ts # Auth config (callbacks, pages)
│ ├── api-helpers.ts # withErrorHandler, requireTeamMember, etc.
│ ├── prisma.ts # Prisma client (lazy proxy)
│ ├── s3.ts # S3 utilities
│ ├── resend.ts # Email client
│ ├── query-client.ts # React Query
│ ├── utils.ts # cn(), getInitials(), etc.
│ └── jobs/ # pg-boss setup
├── proxy.ts # Security headers (NOT auth)
├── types/
│ └── next-auth.d.ts # Auth type extensions
└── generated/
└── prisma/ # Prisma client outputsrc/
├── app/
│ ├── (auth)/ # 公开的身份验证页面(登录、注册)
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx # 客户端组件,无身份验证
│ ├── (dashboard)/ # 受保护的页面
│ │ ├── layout.tsx # 服务端组件,身份验证检查
│ │ ├── dashboard-shell.tsx # 侧边栏 + 导航
│ │ └── teams/[teamId]/
│ ├── invite/[token]/page.tsx # 邀请接受页面
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── teams/
│ │ │ ├── route.ts # 团队列表/创建
│ │ │ └── [teamId]/
│ │ │ ├── route.ts # 团队CRUD
│ │ │ ├── members/
│ │ │ └── invitations/
│ │ ├── invitations/
│ │ │ └── [token]/accept/route.ts
│ │ └── uploads/[...path]/route.ts # S3代理
│ ├── globals.css # 设计系统
│ ├── layout.tsx # 根布局,包含Providers
│ └── page.tsx # 着陆页
├── components/
│ ├── ui/ # shadcn组件
│ ├── teams/ # 团队管理组件
│ ├── invitations/ # 邀请相关UI
│ ├── shared/ # 可复用组件
│ ├── providers.tsx # 应用提供者
│ └── theme-toggle.tsx # 主题切换器
├── hooks/
│ ├── use-teams.ts # 团队相关hooks
│ └── ...
├── lib/
│ ├── auth.ts # NextAuth配置(Node.js运行时)
│ ├── auth.config.ts # 身份验证配置(回调、页面)
│ ├── api-helpers.ts # withErrorHandler、requireTeamMember等
│ ├── prisma.ts # Prisma客户端(懒加载代理)
│ ├── s3.ts # S3工具类
│ ├── resend.ts # 邮件客户端
│ ├── query-client.ts # React Query配置
│ ├── utils.ts # cn()、getInitials()等工具函数
│ └── jobs/ # pg-boss配置
├── proxy.ts # 安全头配置(非身份验证)
├── types/
│ └── next-auth.d.ts # 身份验证类型扩展
└── generated/
└── prisma/ # Prisma客户端输出| Asset | Description |
|---|---|
| NextAuth v5 with Credentials provider |
| Auth config (callbacks, custom pages) |
| withErrorHandler, requireTeamMember, ApiError |
| Lazy-loaded Prisma client with pg adapter |
| S3 utilities with presigned URLs |
| Resend email client + templates |
| React Query setup |
| pg-boss job queue |
| Utility functions |
| App providers |
| Light/dark/system toggle |
| Team dropdown |
| Linear-style design system |
| Production Docker build |
| Railway deployment |
| Next.js configuration |
| Security headers |
| Environment variables |
| shadcn configuration |
| Base team schema |
| Team API template |
| Invitations API template |
| React Query hooks |
| Type extensions |
| 资源文件 | 描述 |
|---|---|
| 带有Credentials提供者的NextAuth v5配置 |
| 身份验证配置(回调、自定义页面) |
| withErrorHandler、requireTeamMember、ApiError |
| 带有pg适配器的懒加载Prisma客户端 |
| 带有预签名URL的S3工具类 |
| Resend邮件客户端 + 模板 |
| React Query配置 |
| pg-boss任务队列 |
| 工具函数 |
| 应用提供者 |
| 明暗/系统主题切换器 |
| 团队下拉选择器 |
| Linear风格设计系统 |
| 生产环境Docker构建文件 |
| Railway部署配置 |
| Next.js配置 |
| 安全头配置 |
| 环境变量示例 |
| shadcn配置 |
| 基础团队Schema |
| 团队API模板 |
| 邀请API模板 |
| React Query hooks |
| 类型扩展 |
bunx create-next-app@latest my-saas --typescript --tailwind --eslint --app --src-dir
cd my-saasbunx create-next-app@latest my-saas --typescript --tailwind --eslint --app --src-dir
cd my-saasundefinedundefinedundefinedundefined.env.exampleundefined.env.exampleundefinedundefinedundefinedundefinedundefinedundefinedundefinedassets/assets/#8b5cf6#8b5cf6#ffffffhsl(240 10% 10%)hsl(240 6% 90%)#ffffffhsl(240 10% 10%)hsl(240 6% 90%)#0A0A0Bhsl(0 0% 95%)rgba(255,255,255,0.06)#0A0A0Bhsl(0 0% 95%)rgba(255,255,255,0.06)next-themesattribute="class"next-themesattribute="class"paramsPromisetype RouteParams = {
params: Promise<{ teamId: string }>;
};
export async function GET(req: NextRequest, { params }: RouteParams) {
const { teamId } = await params; // MUST await!
// ...
}paramsPromisetype RouteParams = {
params: Promise<{ teamId: string }>;
};
export async function GET(req: NextRequest, { params }: RouteParams) {
const { teamId } = await params; // 必须await!
// ...
}import { NextRequest, NextResponse } from "next/server";
import { requireTeamMember, parseJsonBody, withErrorHandler, ApiError } from "@/lib/api-helpers";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
const createSchema = z.object({
name: z.string().min(1),
});
type RouteParams = {
params: Promise<{ teamId: string }>;
};
export const POST = withErrorHandler(async (req: NextRequest, { params }: RouteParams) => {
const { teamId } = await params;
// Auth check - throws 401/403
await requireTeamMember(teamId);
// Parse and validate body - throws 400
const { name } = await parseJsonBody(req, createSchema);
// Business logic
const result = await prisma.someModel.create({
data: { name, teamId },
});
return NextResponse.json(result, { status: 201 });
});import { NextRequest, NextResponse } from "next/server";
import { requireTeamMember, parseJsonBody, withErrorHandler, ApiError } from "@/lib/api-helpers";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
const createSchema = z.object({
name: z.string().min(1),
});
type RouteParams = {
params: Promise<{ teamId: string }>;
};
export const POST = withErrorHandler(async (req: NextRequest, { params }: RouteParams) => {
const { teamId } = await params;
// 身份验证检查 - 抛出401/403错误
await requireTeamMember(teamId);
// 解析并校验请求体 - 抛出400错误
const { name } = await parseJsonBody(req, createSchema);
// 业务逻辑
const result = await prisma.someModel.create({
data: { name, teamId },
});
return NextResponse.json(result, { status: 201 });
});// Factory pattern for query keys
export const teamKeys = {
all: ["teams"] as const,
lists: () => [...teamKeys.all, "list"] as const,
details: () => [...teamKeys.all, "detail"] as const,
detail: (id: string) => [...teamKeys.details(), id] as const,
members: (id: string) => [...teamKeys.detail(id), "members"] as const,
};// 工厂模式生成查询键
export const teamKeys = {
all: ["teams"] as const,
lists: () => [...teamKeys.all, "list"] as const,
details: () => [...teamKeys.all, "detail"] as const,
detail: (id: string) => [...teamKeys.details(), id] as const,
members: (id: string) => [...teamKeys.detail(id), "members"] as const,
};export function useCreateTeam() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createTeam,
onSuccess: (newTeam) => {
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
queryClient.setQueryData(teamKeys.detail(newTeam.id), newTeam);
},
});
}export function useCreateTeam() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createTeam,
onSuccess: (newTeam) => {
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
queryClient.setQueryData(teamKeys.detail(newTeam.id), newTeam);
},
});
}// Create job
await boss.send(QUEUES.SEND_EMAIL, payload, DEFAULT_JOB_OPTIONS);
// Worker handles job
boss.work(QUEUES.SEND_EMAIL, { batchSize: 1 }, async (job) => {
await handleSendEmail(job.data);
});// 创建任务
await boss.send(QUEUES.SEND_EMAIL, payload, DEFAULT_JOB_OPTIONS);
// 处理器处理任务
boss.work(QUEUES.SEND_EMAIL, { batchSize: 1 }, async (job) => {
await handleSendEmail(job.data);
});// Schedule recurring jobs on worker startup
await boss.schedule(QUEUES.CLEANUP, "0 6 * * *", {}); // Daily at 6 AM UTC// 处理器启动时设置重复任务
await boss.schedule(QUEUES.CLEANUP, "0 6 * * *", {}); // 每天UTC时间6点执行FROM node:22-alpine
RUN npm install -g bunFROM node:22-alpine
RUN npm install -g bunundefinedundefined[build]
builder = "dockerfile"
[deploy]
healthcheckPath = "/"
healthcheckTimeout = 300
restartPolicyType = "on_failure"[build]
builder = "dockerfile"
[deploy]
healthcheckPath = "/"
healthcheckTimeout = 300
restartPolicyType = "on_failure"async rewrites() {
return [
{
source: "/uploads/:path*",
destination: "/api/uploads/:path*",
},
];
}async rewrites() {
return [
{
source: "/uploads/:path*",
destination: "/api/uploads/:path*",
},
];
}