关于 cofounder-tech-stack
定义 Go API 与 React SPA 项目的架构和开发实践的 Skill,涵盖单二进制部署、公开页面构建时预渲染、项目文档及密钥处理。
主要功能
- 01Go API 与 React SPA 架构
- 02单二进制部署指引
- 03公开路由构建时预渲染
- 04项目文档规范
- 05本地密钥处理指引
适用场景
- 01规划 Go 与 React 应用
- 02实现公开页面预渲染
- 03维护项目架构文档
- 04配置本地开发密钥
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ locaweb/ cofounder --skill cofounder-tech-stack安装后在 Claude Code 中输入/cofounder-tech-stack确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/google-auth.mdnotify-patterns.mdpg-cron.mdpg-jsonschema.mdpgmq.mdpgroonga.mdpgvector.mdsmtp-gateway.md详细介绍
技术栈
Go JSON API + React SPA,由单个二进制文件提供服务,并作为一个容器部署。构建时会将公开页面预渲染为静态 HTML,以便搜索引擎和 AI Agent 读取。
架构
后端: Go 标准库 net/http 路由器,使用 pgx/v5 连接 Postgres,使用 sqlc 生成查询,使用 slog 记录日志,并通过 go:embed 嵌入 SQL 迁移文件。
前端: React + TypeScript、shadcn/ui 组件、Tailwind CSS,使用 React Router 框架模式并设置 ssr: false——运行时为纯 SPA(生产环境中没有 Node 服务端),底层开发服务器和打包工具使用 Vite。公开路由列在 prerender 配置中,因此会以真实 HTML 的形式发布,供搜索引擎和 AI Agent 使用(参见公开页面:为 SEO 和 AI Agent 预渲染)。编写或修改前端代码时,始终使用 frontend-design 技能——它为布局、样式、组件美感和 UI 润色提供设计方向。
项目布局
.
├── backend/
│ ├── cmd/server/main.go # Entrypoint
│ ├── internal/
│ │ ├── config/ # Env var parsing
│ │ ├── database/
│ │ │ ├── migrations/*.sql # Embedded, forward-only
│ │ │ ├── queries/*.sql # sqlc source
│ │ │ └── sqlc/ # Generated — do not edit
│ │ └── handler/ # HTTP handlers (JSON API)
│ ├── go.mod
│ ├── go.sum
│ └── sqlc.yaml
├── e2e/ # Visual checks (Playwright screenshots)
│ └── package.json
├── frontend/ # React SPA (React Router framework mode)
│ ├── app/
│ │ ├── routes.ts # Route declarations
│ │ ├── root.tsx # Root layout (head, favicon, global providers)
│ │ ├── routes/ # Route modules
│ │ └── components/ui/ # shadcn/ui (generated, editable)
│ ├── react-router.config.ts # ssr: false (+ prerender list of public routes)
│ └── build/ # Build output (gitignored); static assets in build/client/
└── Dockerfile
确保项目 .gitignore 至少包含:
backend/server # add the actual output path used by `go build`
frontend/build/
frontend/.react-router/ # generated route types
frontend/node_modules/
e2e/node_modules/
.venv/
.env
backend/cmd/server/server 是通过 go build 在本地编译生成的 Go 二进制文件,不得提交。
.env 保存本地运行所需的所有服务连接字符串和应用机密。绝不可提交此文件。创建或更新后,请限制文件权限:
chmod 0600 .env
示例内容:
DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
BASE_URL="http://localhost:5173"
REDIS_URL="redis://localhost:6379"
N8N_WEBHOOK_URL="http://localhost:5678/webhook"
始终使用双引号括起值——某些值(例如 SMTP 应用密码)包含空格。
Go 后端通过 os.Getenv() 读取这些值。本地开发时,在启动服务之前使用 . .env 加载文件(POSIX 点号语法——不要使用 source,它是 bash 内置命令,可能无法在所有 shell 中使用):
set -a && . .env && set +a
这样可以弥合本地和部署环境之间的差异:本地由 .env 提供值;部署环境则由 Kamal 配置提供值。Go 代码保持不变(os.Getenv("REDIS_URL"))。
在本地 .env 文件中填写机密
绝不要通过聊天询问机密值。
由 Agent 生成的机密(JWT 密钥、会话机密、HMAC 密钥等)——直接生成并写入:
mise x -- python -c "import secrets; print(secrets.token_urlsafe(32))"
由用户提供的机密(OAuth 凭据、SMTP 密码、API 密钥等):
-
将用户机密设为
REPLACE_WITH_占位符并写入.env。例如:DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" JWT_SECRET="<already generated by agent>" GOOGLE_CLIENT_ID="REPLACE_WITH_GOOGLE_CLIENT_ID" GOOGLE_CLIENT_SECRET="REPLACE_WITH_GOOGLE_CLIENT_SECRET" SMTP_PASSWORD="REPLACE_WITH_SMTP_APP_PASSWORD" -
使用完整绝对路径在用户的编辑器中打开
.env。不要提供在聊天中粘贴值的替代方案。根据用户的平台选择 合适的命令——在 Linux 上通过以下方式检测 WSL:{ grep -qi microsoft /proc/version 2>/dev/null \ || grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; } \ && echo WSL平台 命令(由 Agent 直接运行) macOS open -a TextEdit /Users/name/project/.envWindows(原生,git bash) start notepad "C:\Users\name\project\.env"Windows 上的 WSL notepad.exe /home/name/project/.env(WSL 会自动映射路径)Linux(原生) 请用户在单独的终端中运行 nano <ABSOLUTE_PATH>/.env。 -
引导用户获取每项凭据(适用时使用参考文档:
references/google-auth.md、references/smtp-gateway.md),并明确告知不要将值粘贴到聊天中。请用户保存并确认。 -
验证:读取
.env,检查REPLACE_WITH_前缀是否已移除。绝不要输出实际机密值。
mise.toml 不应加入 git 忽略规则——它会提交到仓库,以便所有开发者使用相同版本的工具。
项目文档
在项目的 docs/ 目录中维护以下文档:
docs/PRD.md — 产品需求文档
描述 Web 应用提供什么,而非如何实现。必须独立于技术实现。
章节: 概述、目标用户、核心功能、用户流程、非功能性需求、范围之外。
收集需求时:
- 如果需求模糊、不完整、有歧义或相互矛盾,帮助用户补全。
- 根据 Web 应用的场景,提出用户可能没有想到的合理想法。
- 随着需求演进,始终保持 PRD 最新。
docs/TASKS.md — 开发任务跟踪表
根据 PRD 生成,反映开发阶段。任务必须考虑可用技能的技术背景。
每项任务的格式: 任务名称 | 状态(Pending / In Progress / Done / Blocked)| 原因/备注
随着工作推进,保持内容最新。
docs/adr/NNN-topic.md — 架构决策记录
为每项技术决策创建编号的 Markdown 文件。
模板:
# NNN - Title
**Status:** Accepted | Rejected | Superseded by [ADR-NNN]
## Context
[What situation or requirement prompted this decision]
## Decision
[What was decided]
## Rationale
[Why this choice was made]
## Trade-offs
**Pros:**
- ...
**Cons:**
- ...
## Alternatives Considered
- [Alternative 1]: [Why discarded]
- [Alternative 2]: [Why discarded]
重点说明为何做出某项选择、考虑过哪些方案,以及舍弃了哪些方案。无需深入介绍实现细节——代码本身就是文档。
docs/INFRASTRUCTURE.md — 服务清单
列出应用依赖的所有 Go 二进制文件之外的服务。引入第一个附属服务时创建;对于不依赖外部服务的应用,该清单可以为空。
格式:
| 名称 | 镜像 | 本地端口 | 环境变量 | 类型 |
|---|---|---|---|---|
| db | supabase/postgres:17.6.1.111 | 5432 | DATABASE_URL | backend |
| redis | redis:7-alpine | 6379 | REDIS_URL | backend |
| n8n | n8nio/n8n:latest | 5678 | — | standalone |
类型: backend = Web 应用/工作进程通过环境变量使用的服务。standalone = 用户在浏览器中直接访问的服务(不与 Go 集成)。
关键决策
单二进制服务
Go 服务处理所有内容:/api/ 下的 API 路由、前端静态文件和 SPA 路由。在开发环境中,Vite 开发服务会将 API 调用代理到 Go 后端。
在本地开发时,Vite 开发服务是面向用户的入口——所有浏览器流量都会经过它,代理会透明地将 /api/* 和 /auth/* 转发到 Go。这意味着,面向外部的 URL(包括在第三方控制台(例如 Google Cloud)中配置的 OAuth 重定向 URI)必须使用 Vite 端口,而不是 Go 后端端口。在 .env 中将 BASE_URL 设置为 Vite 开发服务的源(例如 http://localhost:5173)。在部署环境中,通过部署配置将 BASE_URL 设置为应用的公开 URL(例如 https://myapp.example.com)——参见 app-deploy 技能。在所有情况下,Go 后端都会从环境中读取 BASE_URL——无需推断 Host 标头。
SPA 全部匹配路由不得为每个非 API 路径都提供 SPA 外壳。这样会导致 /assets/ 下的 .js、.css 和其他带哈希的资源以 text/html 内容类型返回,浏览器会因 MIME 类型错误而拒绝这些资源——即使应用在开发环境中正常运行(因为此时 Vite 开发服务会直接处理资源),在生产环境中也会完全无法使用。请始终使用下面的模式——它涵盖了经过实践验证的三条规则:实际文件通过 http.FileServer 提供(内容类型正确);预渲染页面(about/index.html)会直接提供,因为将目录交给 FileServer 会把每个站点地图 URL 都 301 重定向到带尾部斜杠的形式;目录绝不交给 FileServer,否则会暴露可浏览的应用内部文件列表。http.Dir 会限制访问指定目录,因此可防止路径遍历。
SPA 外壳文件名取决于预渲染配置(参见公开页面:为 SEO 和 AI Agent 进行预渲染):不含 prerender 列表的构建会将外壳输出为 index.html;对 / 进行预渲染的构建(任何包含公开页面的应用)会将预渲染后的主页输出为 index.html,并将外壳输出为 __spa-fallback.html。下面的模式会在启动时检测一次——而且 / 必须始终由 index.html 提供(当 / 经过预渲染时,该文件就是主页,而不是外壳)。
**
frontendDist必须是字面字符串"frontend/dist"。**绝不能使用"../frontend/dist",绝不能使用"/frontend/dist"这样的绝对路径,也绝不能添加尝试多个路径的回退逻辑。生产环境中二进制文件的工作目录是 Dockerfile 中的WORKDIR——而不是backend/。查看本地仓库布局时,../frontend/dist看起来似乎正确(Go 从backend/运行),但在 Docker 容器中,它会越过正确的父目录,导致 404。此错误在本地开发期间不会显现,因为 Vite 会提供前端,而且设置了DEV_MODE时不会注册静态文件处理程序——只有部署后才会暴露这个问题。下面的 Dockerfile 部分展示了对应的目录布局。
frontendDist := "frontend/dist"
if _, err := os.Stat(frontendDist); err == nil && !cfg.DevMode {
// SPA shell: __spa-fallback.html when the build prerenders "/", index.html otherwise
spaShell := "index.html"
if _, err := os.Stat(filepath.Join(frontendDist, "__spa-fallback.html")); err == nil {
spaShell = "__spa-fallback.html"
}
fs := http.FileServer(http.Dir(frontendDist))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/auth/") {
http.NotFound(w, r)
return
}
// "/" always maps to index.html (the prerendered home page in prerendered builds)
if r.URL.Path == "/" {
fs.ServeHTTP(w, r)
return
}
target := filepath.Join(frontendDist, filepath.Clean(r.URL.Path))
if info, err := os.Stat(target); err == nil {
if !info.IsDir() {
fs.ServeHTTP(w, r) // real asset; FileServer sets Content-Type
return
}
// Prerendered page ("/about" → about/index.html). Serve the file directly:
// delegating a directory to FileServer would 301 to "/about/", adding a
// redirect hop to every URL listed in sitemap.xml.
page := filepath.Join(target, "index.html")
if _, err := os.Stat(page); err == nil {
http.ServeFile(w, r, page)
return
}
// Directory without index.html (e.g. /assets/): fall through to the shell —
// FileServer would render a browsable listing of the app's internals.
}
// Unmatched route → SPA shell
http.ServeFile(w, r, filepath.Join(frontendDist, spaShell))
})
} else if !cfg.DevMode {
slog.Warn("frontend dist not found — SPA routes will return 404", "path", frontendDist)
}
公共页面:为 SEO 和 AI Agent 预渲染
客户端渲染的 SPA 对大多数非浏览器读取器来说实际上是不可见的。Googlebot 确实会执行 JavaScript,但会延迟到第二轮索引,并受到抓取预算惩罚;Bing 的表现不稳定;而 AI 爬虫(GPTBot、ClaudeBot、PerplexityBot、CCBot)和链接预览工具(WhatsApp、Slack、LinkedIn)会获取原始 HTML,完全不执行 JavaScript——它们看到的只是空的 <div id="root">。这只影响公共页面:对已通过身份验证的应用进行服务器渲染并无益处。
每个应用都已搭建为框架模式(参见前端脚手架与清理),所以只需调整一个配置项——在 prerender 中列出所有公共路由(首页、定价、关于、博客、文档、公共列表):
// frontend/react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false, // pure SPA at runtime — no Node server, ever
prerender: ["/", "/about", "/pricing"], // every public route
} satisfies Config;
完全置于登录保护后的应用(内部工具、仪表板)只需省略 prerender——其他方面无需任何更改。
构建时,React Router 会将列出的每条路由渲染为客户端构建目录中的静态 HTML:index.html(预渲染后的主页——始终将 / 纳入列表)、about/index.html 等;此外,每条路由还会生成一个用于客户端导航的小型 .data 文件。由于 / 已预渲染,SPA 外壳会输出为 __spa-fallback.html——上文的 Go 全匹配处理程序会自动检测到它。浏览器会将预渲染的 HTML 水合为普通 SPA,因此客户端导航和应用体验不受影响;爬虫和 Agent 则能获取完整的 HTML。
**切勿为了 SEO 引入 SSR 框架(Next.js、Remix)或 Node 服务端运行时。**预渲染仅在构建时进行,并保留单个 Go 二进制文件的部署方式。
**ssr: false 的限制:**服务端 loader/action 函数只能用于预渲染路由(它们会在构建时运行,例如读取博客的本地 Markdown 文件)。所有其他路由必须像往常一样使用 clientLoader/clientAction,或通过普通的 fetch 请求 Go API。
每条路由的元数据
每个公共路由模块都必须导出 meta,并包含真实的标题、描述和 Open Graph 标签——这是搜索摘要和链接预览正常工作的关键(普通 SPA 完全无法做到这一点):
export function meta() {
return [
{ title: "Acme — Effortless invoicing" },
{ name: "description", content: "Send invoices in seconds, get paid faster." },
{ property: "og:title", content: "Acme — Effortless invoicing" },
{ property: "og:description", content: "Send invoices in seconds, get paid faster." },
{ property: "og:image", content: "https://acme.example.com/og.png" },
];
}
动态公共内容
构建时预渲染只覆盖构建时已知的路径。prerender 也接受异步函数,因此可以在构建时枚举存储在仓库中的内容(例如以 Markdown 文件形式保存的博客文章)。对于运行时创建的公共页面(公共列表、个人资料、共享文档),请根据实际需求选择:
- **轻量方案(默认):**保持内容由客户端渲染,但让这些路由的 Go 处理程序在提供 SPA 外壳之前,注入正确的
<title>、meta description、OG 标签和 JSON-LD。这样无需任何渲染基础设施,索引和链接预览也能获取必要信息。 - 完整方案:使用
html/template在 Go 服务端渲染这些路由,并复用已构建的 Tailwind CSS(将模板文件加入 Tailwind 的源文件 glob,确保包含其中的类)。仅当公共内容页面本身就是产品时才采用此方案(例如市场平台、目录、招聘网站)。
Bot 规范检查清单
对于拥有公开页面的每个应用,都应使用 Go 处理程序从 BASE_URL 推导出每个 URL,并提供 robots.txt、sitemap.xml 和 llms.txt。切勿将它们写成静态文件:公开源站要等到部署后才会存在,因此静态文件必然会带有一个虚构的域名,导致爬虫在生产环境中无法发现内容,而且这一问题不易察觉。通过针对 两个合成源站(.test 域名)运行的测试来验证 URL 推导逻辑——如果只有一个源站,硬编码的 URL 可能会碰巧通过测试。
robots.txt— 不得禁止 AI 爬虫(GPTBot、ClaudeBot、PerplexityBot、CCBot)抓取;文件末尾应为Sitemap: <BASE_URL>/sitemap.xml。sitemap.xml— 包含每个公开 URL。llms.txt— 面向 LLM Agent 的公开内容纯 Markdown 索引。- JSON-LD 结构化数据(写在路由的 HTML 中,而非 Go 代码中),用于映射到 schema.org 类型(Product、Article、FAQPage、Organization)的页面。
优先使用 PostgreSQL
PostgreSQL 是首选的外部服务。请尽可能使用基于 Postgres 的替代方案:
- 队列:
pgmq— 轻量级消息队列,支持可见性超时、归档和批量操作。参见 references/pgmq.md - Pub/Sub:
LISTEN/NOTIFY— 无需扩展;与持久化层结合使用以确保持久性。参见 references/notify-patterns.md - 缓存: 未记录表
- 调度:
pg_cron+pg_net— 在数据库中运行 cron,并异步发送 HTTP 请求,按计划触发应用端点。优先于容器级 cron。 参见 references/pg-cron.md - 搜索:
pgroonga— 支持包括 CJK 在内的所有语言的全文搜索,并提供布尔查询、排序、词条高亮和 JSONB 搜索。无需配置。当应用涉及可搜索内容(产品、文章、列表、消息、日志)时,主动建议添加搜索功能。只有在针对单一且支持良好的语言,简单的内置方案就足够时,才回退到原生tsvector/tsquery。参见 references/pgroonga.md - 向量:
pgvector— 支持使用 HNSW 和 IVFFlat 索引存储嵌入并进行相似度搜索。参见 references/pgvector.md - JSON 验证:
pg_jsonschema— 通过 CHECK 约束,根据 JSON Schema 验证json/jsonb列。参见 references/pg-jsonschema.md - 地理空间:
postgis— 几何类型、空间索引和地理函数。参见 https://postgis.net/ - 从 SQL 发起 HTTP 请求:
pg_net— 从 SQL 异步发起 HTTP/HTTPS 请求;可与pg_cron配合进行计划调用,也可从触发器中用于发送 Webhook - 其他值得关注的扩展:
pgjwt、pg_stat_statements、pgaudit、pg_hashids
supabase/postgres 中捆绑的 60 多个扩展均可使用。
当应用需要 Postgres 及其扩展无法提供的功能时——例如 n8n 这样的预构建工具、Kafka 这样的专用系统,或明显更适合使用专用服务的场景——请将其作为附属服务添加。有关本地运行方法,请参见 Local Services;有关记录方法,请参见 docs/INFRASTRUCTURE.md。部署时,每个附属服务都会分配到自己的 VM(参见 app-deploy 技能)。
所有查询均使用 sqlc
所有 SQL 都存放在 backend/internal/database/queries/*.sql 中。更改后,运行 cd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generate。切勿在 Go 处理程序代码中编写原始 SQL 字符串。
始终在 sqlc.yaml 中加入 emit_json_tags: true,确保生成的 Go 结构体带有小写 JSON 标签(例如 json:"id",而不是原样导出 ID)。如果不这样做,API 返回的字段名会采用 PascalCase,与前端的预期不符。
启动时迁移
在服务器接受流量前,按顺序应用嵌入的 SQL 文件。迁移只向前执行,并按顺序编号(001_create_users.sql、002_add_tasks.sql,……)。尽可能让每个迁移具备幂等性(CREATE TABLE IF NOT EXISTS、CREATE INDEX IF NOT EXISTS)。
go:embed 指令只接受与声明它的文件位于同一目录或其子目录中的文件——编译器会拒绝包含 .. 的路径。请将嵌入指令放在与 migrations/ 目录相邻的 Go 文件中(例如 backend/internal/database/migrate.go),而不是 cmd/server/main.go。
数据库连接重试
Go 后端应在启动时重试连接数据库(最多 6 次,采用指数退避:1 秒、2 秒、4 秒、8 秒、16 秒、32 秒——总计约 63 秒)。这可应对并行启动的情况——Preview 会同时启动所有服务器,因此后端可能早于数据库就绪——对生产部署而言,这也是一种良好实践。
var pool *pgxpool.Pool
for i := range 6 {
pool, err = pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err == nil {
if err = pool.Ping(ctx); err == nil {
break
}
pool.Close()
}
delay := time.Second * (1 << i) // 1s, 2s, 4s, 8s, 16s, 32s
slog.Warn("database not ready, retrying", "attempt", i+1, "delay", delay, "err", err)
time.Sleep(delay)
}
if err != nil {
slog.Error("failed to connect to database", "err", err)
os.Exit(1)
}
通过 SSE 实时更新
Go 后端监听 Postgres NOTIFY 事件,并为每个已连接的客户端保持一个标准 HTTP 响应处于打开状态,响应的 Content-Type 为 text/event-stream。React 前端使用浏览器内置的 EventSource API。相比 WebSockets,SSE 更可取,因为它能避免代理配置不当造成的问题。
前端脚手架与清理
使用以下命令以非交互方式搭建前端脚手架:
bash -c 'cd "$(git rev-parse --show-toplevel)" && mise x -- npx create-react-router@latest frontend --yes --no-git-init --no-install --no-agent-skills'
该模板预先配置了 Tailwind CSS,并附带以下 npm 脚本:dev(react-router dev——底层使用 Vite)、build(react-router build)和 typecheck(react-router typegen && tsc)。路由在 app/routes.ts 中声明:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("pricing", "routes/pricing.tsx"),
] satisfies RouteConfig;
然后在编写应用代码前进行以下清理:
-
在
react-router.config.ts中设置ssr: false——模板默认值为ssr: true,这需要 Node 服务器;此技术栈不会运行 Node 服务器 -
在
vite.config.ts中添加开发代理,让开发服务器将后端路由转发到 Go(保留现有的plugins和resolve条目)。目标必须与.env中的 GoPORT相匹配:server: { proxy: { "/api": "http://localhost:8080", "/auth": "http://localhost:8080", // Served by Go, derived from BASE_URL — without the proxy they 404 only in dev "/robots.txt": "http://localhost:8080", "/sitemap.xml": "http://localhost:8080", "/llms.txt": "http://localhost:8080", }, }, -
删除
frontend/Dockerfile、frontend/.dockerignore和frontend/README.md——项目根目录负责存放 Dockerfile(参见 Dockerfile),而模板 README 描述的是此技术栈不会使用的 SSR 配置 -
删除
app/welcome/,并将默认主页路由(app/routes/home.tsx)替换为应用的实际主页 -
移除
start脚本和@react-router/serve——SPA 模式不会运行 Node 服务器。保留@react-router/node和isbot:构建时预渲染仍需要它们来解析服务器运行时;移除它们会导致react-router build和typegen出错 -
删除
public/favicon.ico和app/root.tsx中模板自带的 Google Fonts(Inter)链接——frontend-design 技能负责处理 favicon 和字体排版 -
通过
meta导出(根路由或主页路由)设置真实的标题和描述——切勿发布模板默认值
不要创建占位文件 public/favicon.svg——frontend-design 技能会单独处理 favicon 生成。
部署约束
以下要求与 app-deploy 技能要求一致:
- 始终从
PORT环境变量获取端口(部署的应用设置为80,本地开发设置为8080) - 健康检查:
GET /up→ HTTP 200 - 通过
DATABASE_URL(首选)或单独的POSTGRES_*环境变量连接数据库——缺失时必须立即失败 - 文件存储路径为
BLOB_STORAGE_PATH(例如/data/blobs) - 使用 PostgreSQL 作为主要数据存储(通过
supabase/postgres捆绑 60 多个扩展)。如果应用需要 Postgres 无法提供的服务,可通过部署技能的 Kamal 层添加额外组件 - 不使用 ORM、React 以外的 JavaScript 框架或 CSS 预处理器
Dockerfile
多阶段构建:(1)使用 Node 构建前端,(2)构建 Go 二进制文件,(3)使用精简的 Alpine 运行时镜像,其中包含二进制文件、前端资源(复制到 frontend/dist/)和 CA 证书。Go 二进制文件内嵌了迁移文件;前端资源与二进制文件一同复制。
Dockerfile 中的 FROM 标签使用与 mise.toml 相同的主版本号,并带有 -alpine 后缀,例如 FROM node:24-alpine 和 FROM golang:1-alpine。Docker Hub 会在构建时将这些浮动标签解析为最新的次要版本或补丁版本,因此 Dockerfile 无需手动查询版本即可与 mise.toml 保持同步。发布新的 Go 或 Node 次要版本或补丁版本后,下次构建会自动采用该版本。GHA BuildKit 缓存会检测清单变更,并从对应层开始重新构建。
运行时阶段必须将二进制文件和前端客户端构建产物(复制到 frontend/dist)作为同级文件放在 WORKDIR 下:
# Stage 1: Build frontend
FROM node:24-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Build backend
FROM golang:1-alpine AS backend-build
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN CGO_ENABLED=0 go build -o /server ./cmd/server
# Stage 3: Runtime — binary and frontend assets as siblings under WORKDIR
FROM alpine:3
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=backend-build /server ./server
COPY --from=frontend-build /app/frontend/build/client ./frontend/dist
EXPOSE 80
ENV PORT=80
CMD ["./server"]
WORKDIR 的具体路径无关紧要,关键是二进制文件和前端资源都要直接放在该目录下。前端构建产物输出到 frontend/build/client/;Dockerfile 会将该目录复制到 frontend/dist,这是 Go 二进制文件预期使用的固定路径(见上文的 SPA 全路径处理)。由于 CMD 会从 WORKDIR 运行,将两者复制到 WORKDIR 即可满足要求。在本地开发时,Vite 会提供前端服务,因此只有在未设置 DEV_MODE 时才会注册 Go 静态文件处理程序。
不要创建 .dockerignore 文件。 多阶段构建已经能让最终镜像保持精简;如果 .dockerignore 意外排除了 go:embed 所需的文件(例如 migrations/),就会导致构建失败,而且编写时不会出现明确的错误提示。
本地开发
所有工具都通过 mise(由 computer-setup 安装)及 mise x 命令调用。该命令会读取 mise.toml,并运行指定版本的工具,无需激活 shell。数据库以 podman(同样由 computer-setup 安装)运行的 supabase/postgres 容器形式提供,与生产镜像一致。
容器命名约定: 每个项目的数据库容器都命名为
<repo_name>-db(例如myapp-db),其中<repo_name>是项目根目录的基本名称。这样可避免同一台机器上多个联合创始人项目并存时发生冲突。在会话开始时确定一次名称,并在所有podman命令中始终使用该名称。
重要:
go.mod位于backend/,而不是项目根目录。 所有 Go 和 sqlc 命令(mise x -- go run、mise x -- go build、mise x -- go test、mise x -- go mod tidy、mise x -- sqlc generate)都必须从backend/目录执行。务必在bash -c字符串中加入cd "$(git rev-parse --show-toplevel)/backend" &&。如果命令链涉及多层 shell 调用(bash → go),建议编写一个小型辅助脚本,而不要将所有内容嵌套在单个bash -c字符串中,这样可以避免最常见的反复构建失败原因。
项目工具版本
首次设置时(项目根目录中尚不存在 mise.toml 时),手动创建该文件:
[tools]
go = "1"
sqlc = "1"
python = "3.14"
node = "24"
jq = "1"
[settings]
python.compile = false
必须设置 python.compile = false,这样会将 Python 固定为最新的预编译补丁版本,而不是从源代码编译(缺少构建依赖时编译会失败)。
然后信任并安装这些工具:
mise trust
mise install
此 mise.toml 会提交到仓库,确保所有开发者使用相同的版本。mise.toml 仅指定主版本号(例如 go = "1");mise 会在安装时将其解析为最新的稳定次要版本或补丁版本。要更改主版本号,请编辑 mise.toml 并重新运行 mise install。
本指南中的所有工具调用都使用 mise x 命令(例如 mise x -- go run ./cmd/server)。该命令会运行 mise.toml 中指定版本的工具,无需激活 shell 钩子;它可在 Claude Code 的非交互式 shell、Preview 的 launch.json 以及任何脚本上下文中可靠运行。
升级工具
每个会话都运行(这不是服务启动的一部分,而是每次加载此技能时都必须执行的独立步骤)。
-
确保
mise.toml包含上方的[settings]部分;如果缺少,则添加。 -
更新 mise 本身:
# macOS: brew upgrade mise # Linux, WSL, Windows: mise self-update -y -
升级项目工具:
mise upgrade
使用 mise upgrade,而不是 mise install——install 会跳过已安装的版本;upgrade 会检查是否有更新的补丁版本。
1. 启动数据库和本地服务
以下命令使用默认的 Postgres 端口 5432。如果该端口已被主机上的其他进程占用(例如另一个项目的容器,或另一个 Postgres 实例),导致容器无法启动,请选择任意一个空闲端口,更新 .env 中的 DATABASE_URL 以使用该端口,并相应更改 -p 标志。env 文件是事实来源。
# Derive the container name from the repo directory
CONTAINER_NAME="$(basename "$(pwd)")-db"
# Start supabase/postgres container (matching production image)
# Important: provide only the POSTGRES_PASSWORD environment variable. The database is started with both user and database name preset to `postgres`.
# Match the port in DATABASE_URL in .env.
podman run -d \
--name "$CONTAINER_NAME" \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
supabase/postgres:17.11.0.017
# Verify it's ready (uses container exec instead of pg_isready)
podman exec "$CONTAINER_NAME" pg_isready -U postgres
本地服务
当应用需要 Postgres 以外的服务时,请在本地将它们作为 podman 容器运行。每项服务都对应一个配件,部署时会将其配置为专用 VM。
命名约定
每个项目容器都命名为 <repo_name>-<accessory_name>:
myapp-db
myapp-redis
myapp-n8n
Postgres 已采用 -db 约定。请将此约定扩展到所有配件。这样便可使用清理模式(参见停止所有项目容器)。
启动或创建模式
容器会在不同会话之间保留。新会话开始时,上一个会话的容器可能已经存在(但处于停止状态)。请始终使用启动或创建模式,而不是直接使用 podman run:
podman start myapp-redis 2>/dev/null || \
podman run -d --name myapp-redis -p 6379:6379 redis:7-alpine
如果容器存在(无论正在运行还是已停止),podman start 都会静默成功。如果容器不存在,则会使用回退命令 podman run 创建容器。这可以避免恢复会话时出现“名称已被占用”错误。
两种配件类型
连接后端的配件(Redis、Kafka 等):启动或创建 → 就绪检查 → 将环境变量添加到 .env → 添加 Go 客户端 → 在 docs/INFRASTRUCTURE.md 中记录。
独立配件(n8n、WordPress 等):启动或创建 → 就绪检查 → 向用户提供 localhost:<port> 链接 → 在 docs/INFRASTRUCTURE.md 中记录。除非后端也会调用其 API,否则无需 Go 代码。
混合型配件(例如带有 webhook 的 n8n):按独立配件处理(提供浏览器链接),并添加后端环境变量(例如 N8N_WEBHOOK_URL)。类型仍为 standalone;环境变量列表示后端依赖关系。
端口冲突检测
启动容器前,检查 podman ps -a --format '{{.Names}} {{.Ports}}' | grep '<port>'。如果默认端口已被占用,请选择任意一个空闲端口,更新 .env 中的 URL 环境变量(例如 REDIS_URL)以使用该端口,并在容器中公开该端口。.env 文件是事实来源。
常见配置
| 配件 | 镜像 | 端口 | 就绪检查 |
|---|---|---|---|
| Redis | redis:7-alpine | 6379 | podman exec <name> redis-cli ping |
| n8n | n8nio/n8n:latest | 5678 | curl -s http://localhost:5678/healthz |
| Meilisearch | getmeili/meilisearch:latest | 7700 | curl -s http://localhost:7700/health |
| WordPress | wordpress:latest | 8080 | curl -s http://localhost:8080 |
| WAHA | devlikeapro/waha:latest | 3000 | curl -s http://localhost:3000/api/health |
这些仅供参考。Agent 应查阅镜像文档,确认正确的端口和就绪检查端点。
本地与部署环境中的主机名
| 服务 | 本地(.env) | 部署主机名 |
|---|---|---|
| Postgres | localhost:5432 | db:5432 |
| Redis | localhost:6379 | redis:6379 |
| n8n | localhost:5678 | n8n:5678 |
在本地,所有服务都位于 localhost。部署后,每个配件都会获得自己的 VM,主机名通过 CloudStack 内部 DNS 与配件名称对应。服务间通信切勿使用公网 IP。.env(本地)和 Kamal 配置(部署)提供相应的值;Go 通过 os.Getenv() 以相同方式读取这些值。
2. 启动 Go API(终端 1)
bash -c 'ROOT="$(git rev-parse --show-toplevel)" && set -a && . "$ROOT/.env" && set +a && cd "$ROOT/backend" && DEV_MODE=1 mise x -- go run ./cmd/server'
3. 启动前端开发服务器(终端 2)
bash -c 'cd "$(git rev-parse --show-toplevel)/frontend" && mise x -- npm install && mise x -- npm run dev'
不要假设 Vite 使用默认端口(5173)——如果默认端口已被另一个项目占用,Vite 会自动选择下一个可用端口。在后台启动开发服务器后,读取任务输出,并在 Vite 启动横幅中查找 Local: 行(例如,Local: http://localhost:5174/)。后续所有访问都使用该行中的 URL,而不是硬编码端口;同时确保 .env 中的 BASE_URL(以及在其上注册的任何 OAuth 重定向 URI)与实际端口保持一致。
如果任务输出已不可用,请从操作系统检测端口:
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
npm run dev 会运行 react-router dev——底层使用 Vite——并通过脚手架搭建时添加的 server.proxy 块,将 /api/*、/auth/* 和 SEO 文件代理到 Go 后端。
停止所有项目容器
REPO_NAME="$(basename "$(pwd)")"
# Stop and remove all containers for this project
podman ps -a --filter "name=^${REPO_NAME}-" --format '{{.Names}}' | xargs -r podman stop
podman ps -a --filter "name=^${REPO_NAME}-" --format '{{.Names}}' | xargs -r podman rm
此操作依赖命名约定(<repo_name>-<accessory_name>),并会一次性移除所有项目容器。
视觉检查(Playwright 截图)
不要使用 Claude Desktop Preview 服务。请手动启动 Go 后端和 Vite 开发服务器(上文步骤 2–3)。
测试通过后,截取无头浏览器截图并进行视觉检查。适用于所有平台。
设置
项目根目录下的 e2e/ 中有一个包含 playwright 的最简 package.json。首次设置时:
bash -c 'cd "$(git rev-parse --show-toplevel)/e2e" && mise x -- npm install && mise x -- npx playwright install chromium'
e2e/ 目录与 frontend/ 完全独立——Dockerfile 不会触及它,因此不会影响生产镜像。
截取截图
先确认实际使用的 Vite 端口(若 5173 被占用,端口会自动递增):
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
bash -c 'cd "$(git rev-parse --show-toplevel)/e2e" && mise x -- npx playwright screenshot --viewport-size="1280,720" --full-page http://localhost:5173 /tmp/homepage.png'
如果 PRD 有要求,可针对移动设备使用:--viewport-size="375,812"。对于需要身份验证的路由:在 e2e/ 中编写脚本,调用 POST /api/dev/login、获取 cookie、进行导航并截取截图。使用 Read 工具读取截图。
视觉检查范围
- 关键路由(首页、仪表盘、主要功能)。
- 需要身份验证的路由(通过开发登录)。
- 本次工作涉及的页面。
- 美观度——对齐、内边距、间距、可读性、对比度。
修复所有看起来不对的地方,并在提交前重新截图。
本地开发反馈循环
编写代码和测试 → 运行测试 → 视觉检查 → 修复并重复 → 更新文档 → 提交并推送 → 完成收尾。
Write/Edit Code + Tests
↓
Start services (podman, go run, npm run dev)
↓
Run tests (Layer 1: Go, Layer 2: Vitest + npm run typecheck)
↓
All pass? ──No──► Fix & repeat
↓ Yes
Visual check (screenshots)
↓
Looks right? ──No──► Fix & repeat
↓ Yes
Update docs (TASKS, PRD, ADRs, INFRASTRUCTURE)
↓
Commit & push → Wrap up session
提交后,按照 cofounder agent 中的定义,提供本次会话的收尾总结。
sqlc 工作流
务必遵循以下顺序——绝不要跳过第 3 步:
- 在
backend/internal/database/queries/*.sql中编写或更新 SQL 查询 - 运行 sqlc generate:
bash -c 'cd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generate' - 阅读生成的
.go文件(位于backend/internal/database/sqlc/),在编写处理程序前确认确切的结构体和字段名称。sqlc 生成的名称难以预测:
- 带类型转换的位置参数(例如
$2::boolean)会变成Column2、Column3等,而不是列名。 - 使用部分
RETURNING子句的查询会生成单独的行类型(例如UpsertUserRow),与完整模型(User)不同。 - 不阅读输出就假设字段名称,会导致类型不匹配错误,需要来回修正。
- 使用生成文件中的确切名称编写 Go 处理程序。绝不要在 Go 文件中手写 SQL。
约定
- 精简处理器: 解析请求 → 调用数据库 → 返回 JSON。处理器中不包含业务逻辑。
- 日志记录: 只使用
slog。绝不使用fmt.Println或log.Println。 - 验证: 对所有输入进行服务端验证。绝不能只依赖客户端验证。
- 授权: 在每个处理器中进行检查,而不仅仅是在中间件中。
- 前端组件:
bash -c 'cd "$(git rev-parse --show-toplevel)/frontend" && mise x -- npx shadcn@latest add <component>' - 不使用 ORM。 只通过 sqlc 执行 SQL。
- 不使用 CSS 预处理器。 只使用 Tailwind CSS。
- 不添加其他 JavaScript 框架。 只使用 React + React Router。
- 不使用 SSR 框架或 Node 服务端运行时。 SEO 需求通过构建时预渲染满足(参见公开页面:为 SEO 和 AI Agent 预渲染)——绝不要引入 Next.js 或运行时渲染服务端。
授权最佳实践
如果应用有用户登录区域,使用用户名和密码自行登录可以用于快速搭建应用原型,但绝不能用于生产环境——这种方式的安全性较弱。
推荐做法: 原型开发阶段先使用自行登录(用户名 + 密码),然后尽快改用以下一种或两种方式:
- 电子邮件魔法链接——实用且无需记忆密码。需要配置 SMTP 网关。参见references/smtp-gateway.md
- Google Auth——实用且无需记忆密码。依赖 Google 的安全机制。需要配置 Google Auth(免费)。参见references/google-auth.md
用于测试的开发登录
在本地开发期间,Agent 需要测试需通过身份验证才能访问的页面(使用视觉检查中的 Playwright 截图)。魔法链接和 Google Auth 流程无法在自动化测试中完成,因此后端必须提供一个仅用于开发的登录端点,以绕过真实的身份验证流程。
工作原理
后端仅在设置了 DEV_MODE=1 时注册 POST /api/dev/login 路由。必须在路由注册时进行守卫(而不是使用中间件),这样在未设置该标志时,此端点就不会实际存在。
此端点接受用户标识符(例如电子邮件地址),查找用户,并使用与真实身份验证流程完全相同的机制创建会话——使用相同的 Cookie 名称、相同的令牌格式和相同的会话存储。唯一的区别是它会跳过外部身份提供方(魔法链接电子邮件或 Google OAuth)。
测试用户
如果测试用户尚不存在,开发登录处理器应即时创建该用户(INSERT ... ON CONFLICT DO NOTHING)。这样测试用户就不会进入迁移文件,而迁移会在包括生产环境在内的所有环境中运行。
安全性
- 绝不能在未设置
DEV_MODE时注册此端点——通过在路由注册时进行守卫来强制执行。 - 生产部署绝不能设置
DEV_MODE。部署技能不会包含该变量。
其他参考资料
- references/smtp-gateway.md -- SMTP 网关设置:用于发送电子邮件(提醒、身份验证链接、通知等)的 Gmail(原型开发)和 Locaweb(生产环境)
- references/google-auth.md -- Google Auth OAuth 设置:Google Cloud Console 配置、同意屏幕、凭据
Postgres 扩展参考
- references/pgroonga.md -- PGroonga 全文搜索:运算符、排名、高亮、CJK 支持
- references/pgmq.md -- pgmq 消息队列:发送、读取、归档、删除的 SQL 示例
- references/pg-cron.md -- pg_cron + pg_net:定时任务、HTTP 触发器、常见模式
- references/pgvector.md -- pgvector 相似度搜索:距离运算符、HNSW/IVFFlat 索引、调优
- references/pg-jsonschema.md -- pg_jsonschema 验证:CHECK 约束模式、核心函数
- references/notify-patterns.md -- LISTEN/NOTIFY + 持久化:使用 pgmq 实现作业队列、使用常规表记录数据更新、轮询回退方案