内核不含业务代码。业务以模块挂上,按租户开关。人与 Cursor / Claude 走同一条 Spec → gen → check 闭环——不是又一份脚手架。
| 本地启动 | 开箱即用 | 一种部署 |
|---|---|---|
| 三条命令跑起来 | CMS · 认证 · 计费 | 单进程 Docker Compose |
产品站就是默认租户(slug=rewindom):rewindom.com,文档在同一套 CMS 上的 /docs。
| 不是脚手架喷发器 | 不是微服务框架 | 不是低代码平台 |
|---|---|---|
| Agent 写的代码进仓库、过闸门。没有「生成完就扔掉」的一次性骨架。 | 编译期组装、一个进程。租户、官网、工作台按 Host 分流,而不是再拆一套服务。 | 没有天花板:模块是 TypeScript 包,契约与测试都是代码。闸门强制边界,不代替写代码。 |
AGENTS.md 与 Skills 是产品的一部分。新模块填 MODULE.spec,改现有模块填 FEATURE.spec;留空则 Agent 必须追问,禁止瞎猜。
| 填 Spec | 生成并装配 | 机器闸门 |
|---|---|---|
| 声明 id、权限、模型、触达面。空白就是问题,不是发挥空间。 | 骨架与注册表一次写对,避免漏挂路由或租户守卫。 | 契约、依赖、文案校验拦住越权与漏装配;CI 跑同一套。 |
详见 agent-first.md。
按租户开关:未开通不挂路由、不进侧栏、不产生数据。
| 站点 | 平台 | 运营 |
|---|---|---|
| 官网 CMS 与 SSR、文档库、站点会员、自定义域名。主域就是产品站,租户站按 Host 绑定。 | 认证与 JWT、多租户与套餐配额、PBAC、审计、通知、任务中心。 | 错误日志、慢查询与慢请求、平台计费与站点收款。可观测性不是事后再接。 |
内置能力在 packages/builtin/,两处 enabled-modules.ts 手写注册。外部业务在 modules/,由 pnpm gen:external-modules 装进组装层:
| 类型 | 模块 |
|---|---|
| 基础设施 | user · platform · rbac · audit · notification · background-job · error-log · slow-query · slow-request · dashboard · translation · marketing · site-member · billing · site-billing |
| 外部业务 | note(金标准 CRUD)· todo · bookmark · shop · site-docs · site-form · events |
全球科技热点与实时趋势追踪。同一进程、按 Host 分流、自定义域名、events 资讯模块——不是演示站,是线上产品。
这套底座在线上扛着:多租户 CMS 与 SSR · 自定义域名 · events 资讯模块 · 单进程,按 Host 分流。
打开 yestino.com。
先跑起来,再让 Agent 扩模块。clone 仓库,pnpm setup && pnpm dev。文档和产品站是同一套。
前置:Node.js 22+、pnpm 11+、Docker。
git clone https://github.com/vvenv/rewindom && cd rewindom
pnpm install
pnpm setup # 幂等:.env.local + Postgres/Redis + migration
pnpm dev- API
http://localhost:3700(/api由 Vite 代理)
pnpm setup 可重复执行;仅起库:pnpm db:up。
localhost 与 127.0.0.1 不是同一个入口——同一个 Vite 端口,按 Host 分流:
localhost 是产品站(隐式绑定默认租户),127.0.0.1 是平台控制台。这是刻意的,
免去改 /etc/hosts 就能在本地同时验证两种 Host。
| 要进哪儿 | 本地地址 | 说明 |
|---|---|---|
| 租户官网 | http://localhost:7300/ |
默认租户的 CMS 站点(Fastify SSR) |
| 租户工作台 | http://localhost:7300/app |
稳定入口;未登录自动转 /login。所有工作台页面都在 /app/*(/app/site、/app/dashboard…) |
| 租户登录 | http://localhost:7300/login |
租户锁定为默认租户 |
| 站点会员 | http://localhost:7300/member/login |
站点前台的终端客户,与工作台用户是两套身份 |
| 平台控制台 | http://127.0.0.1:7300/platform |
未登录自动转 /login;平台管理员在此登录 |
常见困惑:在 localhost:7300/platform 打不开控制台——那个 Host 绑着租户,控制台不在
上面,会被转到 127.0.0.1:7300/platform。反过来,127.0.0.1:7300/ 也没有官网,
它会直接进控制台入口。
把基域设成 localhost(.env.local),然后直接开 http://{slug}.localhost:7300:
TENANT_BASE_DOMAIN=localhost浏览器原生把 *.localhost 解析到回环地址,不用改 hosts 文件。三个入口互不影响:
localhost 仍是默认租户、127.0.0.1 仍是平台控制台、{slug}.localhost 是对应租户
(app / api / platform 等保留前缀不会被当成租户)。
要验自定义域名那条分支,就把某个租户的 custom_domain 设成 shop.localhost 之类,
同样直接可访问。
刻意不做「开发态把当前 origin 手动绑到某租户」的旁路:那会绕过
resolveHostTenant,让最容易在生产出问题的 Host 解析恰好成为本地唯一不被验证的 环节,还得为它加一道非生产门禁(本质是「按 Host 冒充租户」)。用真域名走真路径更省事。
当前生产(.env.production):APP_DOMAIN=rewindom.com,TENANT_BASE_DOMAIN=rewindom.com。
由 Host 决定,与本地同构:
| 要进哪儿 | 地址 | env |
|---|---|---|
| 产品站 | https://rewindom.com/ | FRONTEND_URL |
| 租户管理台(工作台) | https://rewindom.com/app | 同上 |
| 租户登录 | https://rewindom.com/login | 同上;未登录访问 /app 会转到此页 |
| 其他租户登录 | https://{slug}.rewindom.com/login 或租户 custom_domain 上的 /login |
TENANT_BASE_DOMAIN |
| 租户站点 | 租户 custom_domain 或 https://{slug}.rewindom.com |
TENANT_BASE_DOMAIN |
| 平台控制台 | https://admin.rewindom.com/platform | PLATFORM_URL / PLATFORM_HOST |
PLATFORM_URL 必须与 FRONTEND_URL 不同 Host:nginx 按 Host 分流(见
docker/nginx/default.conf.template 的 $use_tenant_ssr),平台 Host 走静态 SPA,
其余 Host 的 HTML 反代给 Marketing SSR。完整口径见
tenant-config.md 的「自定义域名 / Host 绑定」。
- 默认租户(产品站 Host):打开 https://rewindom.com/login ,或 https://rewindom.com/app (未登录自动转
/login)。 - 其他租户:
https://{slug}.rewindom.com/login,或该租户绑定域名上的/login(Host 锁定租户,可用裸用户名)。 - 凭据:租户 User(工作台账号),与平台管理员不是同一套身份;多租户登录标识见 tenant-config.md。
-
DNS:
admin.rewindom.com需 A/CNAME 指向与rewindom.com相同的服务器;TLS 证书须覆盖该 Host(通配*.rewindom.com或单独签发admin.rewindom.com)。 -
打开 https://admin.rewindom.com/platform (不要用
rewindom.com)。未登录会转到同 Host 的/login。- 入口用
/platform,不要只打开 https://admin.rewindom.com/ ——根路径在控制台 Host 上没有官网,旧版会把自己硬跳进死循环(已修:根路径会转到/platform)。 - 在 https://rewindom.com/platform 会被送到平台 Host。
- 入口用
-
凭据(
.env.production,首次启动写入PlatformAdmin):用户名 vvenv(PLATFORM_ADMIN_USERNAME)密码 .env.production的PLATFORM_ADMIN_PASSWORD登录时不要加
@tenant后缀。平台管理员与租户 User 是两套身份。 -
改密码后生效:若改了 env 密码但库里已有同名管理员,不会自动覆盖——需在控制台改密,或清库后重启让 bootstrap 重建(仅空环境适用)。改 env 后执行
pnpm deploy -- --env production --env-only同步到容器。
编译期组装、单进程部署。每模块自带 server 路由、client 页面、shared 契约与 MODULE.md。内核与基础设施不含业务领域代码。
- 内核不依赖业务 — HTTP 栈、认证、租户上下文、ModuleLoader、EventBus
- 模块间禁止直接 import — 跨模块走 manifest
requires+ Event Bus / Provider / Slot - 模块可按租户开关 — 未开通不挂路由、不进侧栏
- 边界由机器强制 —
pnpm check:deps/check:modules/check:i18n(包层环 + manifest/schema FK + 文案)
- 内置能力改
packages/builtin/<id>/,在两处enabled-modules.ts手写注册 - 业务模块用
pnpm gen:module <spec.yaml>生成到modules/<id>/(经@rewindom/module-sdk门面,不直连内核),再pnpm gen:external-modules装进组装层;金标准是modules/note
前端有四类路由挂载点:renderPublicRoutes(无守卫,租户 CMS 前台)、renderGuestRoutes(登录注册,已登录会被弹走)、renderTenantRoutes(租户应用)、renderPlatformRoutes(平台控制台,仅 PLATFORM_URL Host)。公开页 SEO 由 Fastify SSR 输出;SPA 接管后补交互层。
技术栈没有惊喜:招人好招,出问题搜得到答案。价值在边界与约束,不在堆新框架。
| 层次 | 技术 |
|---|---|
| 后端 | Fastify 5 · TypeScript 6 · Prisma 7 |
| 数据 | PostgreSQL 16 · Redis 7 · BullMQ |
| 前端 | React 19 · Vite 8 · React Router v8 · TanStack Query/Table |
| UI | shadcn/ui · Tailwind CSS 4 |
| 认证 | JWT 双 Token(access + refresh) |
| LLM | OpenAI 兼容(可选,默认 DeepSeek) |
| 部署 | Docker Compose(生产)· 宿主机热更新(开发) |
rewindom/
├── apps/
│ ├── server/ # Fastify 组装 + Prisma schema/migrations
│ │ └── src/enabled-modules.ts # 内置 + …EXTERNAL_SERVER_MODULES
│ └── client/ # React 组装 + 产品壳(登录、Layout、Sidebar)
│ └── src/enabled-modules.ts
├── packages/
│ ├── builtin/ # 内置基础设施(user / platform / billing / marketing …)
│ ├── module-sdk/ # 外部模块门面(禁止直连 server-kernel / client-kit)
│ ├── server-kernel/ # 内核:HTTP、认证、ModuleLoader、EventBus
│ ├── client-kit/ # api、守卫、PageLayout、Slot
│ ├── shared/ · ui/ # 跨端类型 · shadcn 基础组件
│ └── server-test/ · client-test/
├── modules/ # 外部业务包(note / shop / site-docs / events …)
├── docker/ · docs/ · scripts/
内置模块在两处 enabled-modules.ts 手写;外部模块由 pnpm gen:external-modules 生成聚合文件再展开。
| 命令 | 说明 |
|---|---|
pnpm setup / pnpm dev |
本地初始化(幂等)/ 启前后端 |
pnpm db:up / db:down / db:studio |
本地库启停 / Prisma Studio |
pnpm db:pull / seed |
拉取远程库到本地 / 初始化种子数据 |
pnpm build / pnpm start |
构建 / 生产模式启动 |
pnpm test / pnpm check |
测试 / lint + test |
pnpm check:deps / check:modules / check:i18n |
模块边界 / 契约(注册表、权限、nav)/ 客户端文案 |
pnpm gen:module <spec.yaml> |
从 spec 生成外部业务模块骨架(modules/<id>/) |
pnpm gen:external-modules |
聚合 modules/* 到组装层(通常由 gen:module 触发) |
pnpm docker:stack:up |
本地生产 Docker 栈(需 .env.docker.local) |
pnpm bootstrap / deploy / release |
远程首次部署 / 更新 / 发版 |
复制 .env.example → .env.local,至少:
| 变量 | 说明 |
|---|---|
DATABASE_URL |
postgresql://rewindom:...@localhost:5433/rewindom |
JWT_SECRET |
≥32 字符随机串 |
TENANT_SECRET_ENCRYPTION_KEY |
32 字节 hex(openssl rand -hex 32) |
租户级密钥(如 LLM API Key)AES-GCM 加密存库,主密钥为 TENANT_SECRET_ENCRYPTION_KEY。完整列表见 .env.example。
付款(billing / Creem,可选):
| 变量 | 说明 |
|---|---|
CREEM_API_KEY |
Test/Live API Key;空则无法发起 checkout |
CREEM_WEBHOOK_SECRET |
Webhook 验签密钥(与 Dashboard 一致) |
CREEM_SERVER |
test | prod(本地联调用 test) |
CREEM_PRODUCT_MAP |
JSON:{"starter":"prod_xxx",...}(必须是 prod_ 开头的商品 ID,不是套餐 slug) |
CREEM_STORE_ID |
可选;默认见 .env.example |
平台套餐分两层:
| 层 | 内容 | 改在哪 |
|---|---|---|
| 结构 | 有哪几个 slug、配额、功能开关 | 代码 PRICING_PLANS(发版) |
| 运营 | 价格、上架、推荐、排序、名称/卖点文案 | 平台控制台套餐配置(AppSetting.plan_pricing) |
官网段 billing.plans(平台套餐)是数据驱动的:段 settings 只管版式与 CTA,不存第二份价格。SSR 渲染前由 registerSectionContextProvider 按需注入目录;公开只读接口 GET /api/public/plans 与编辑器预览读同一份合并结果(未配置字段回落到代码默认值)。
站点会员定价段 site-billing.plans 读的是站点自建的 MemberPlan,与平台套餐无关——见 packages/builtin/site-billing/MODULE.md。
两条付款域、两套 webhook,都打 API(3700),不是前端(7300)。
| 域 | 谁付钱 | 工作台 / 会员入口 | Webhook |
|---|---|---|---|
平台租户付费(billing) |
组织(工作台用户) | http://localhost:7300/app/billing |
/api/billing/webhooks/creem |
站点会员付费(site-billing) |
站点会员 | http://localhost:7300/member/billing |
/api/site-billing/webhooks/creem |
.env.local配好上表 Creem 变量,pnpm dev- 隧道指向 API:
ngrok http 3700
# 或:cloudflared tunnel --url http://localhost:3700-
Creem Dashboard(Test Mode)→ Developers → Webhooks,Endpoint 填对应路径;Signing secret 与
CREEM_WEBHOOK_SECRET(或站点覆盖密钥)一致;改配置后重启 server。 -
浏览器走对应入口 → 用 test 卡付款。开通以 webhook 为准(看 server 日志),回跳 URL 只是页面返回。
详情见 packages/builtin/billing/MODULE.md 与 packages/builtin/site-billing/MODULE.md。
{ data: T } // 成功
{ data: T[], meta: { total, page, page_size } } // 分页
{ error: string, code?: string } // 错误状态码:200 成功 · 201 创建 · 400 参数 · 401 未授权 · 403 无权限 · 404 不存在 · 409 冲突 · 500 内部错误。
DB / API / 类型 / 列表 URL 用 snake_case,路由 path 参数用 camelCase → field-naming-conventions.md。
生产与测试走 Docker Compose(docker-compose.prod.yml),宿主机 Nginx 终结 SSL。
cp scripts/env.production.example .env.production
pnpm bootstrap -- --env production # 首次
pnpm deploy -- --env production # 更新亦可 git tag v* 触发 GitHub Actions。详见 deployment.md。