-
Notifications
You must be signed in to change notification settings - Fork 45
Changelog 2026 04 17 Self Hosted Infra
一句话总结:Neon 免费额度耗尽触发的连锁重构——数据库迁到自建 PG、 pgAdmin 上线、sa-token cookie 跨子域共享、Infisical 接管密钥管理、 开发者和管理员入口分开、文档归位。
对应 PR:
- backend:#12 Self-hosted PG + pgAdmin + forward_auth gate
- backend:#13 /api/chat/sessions/save 替代前端 Prisma
- backend:feat/devtool-gate(devtool-check 泛化,待合)
- frontend:#301 admin/database + satoken cookie + chat 改 fetch
- frontend:feat/admin-secrets-card(Infisical 入口,待合)
凌晨 01:06 Neon 来邮件说 involution-hell 项目当月 100 CU-hour 计算配额耗尽,
计算节点即将被暂停。业务立刻开始报错:
-
/api/events500(backend 连不上 Neon) - 前端 AI 对话
onFinish里的 prisma 调用失败(前端 Prisma 也指向 Neon) - 管理员后台接口全部 500
原本 Neon 只是"免费的 managed Postgres",挂了之后复盘才发现它同时承担了 很多角色,每一个都是单点:
- 后端 Spring Boot 的主库
- 前端 Prisma 的直连目标(AI 对话、CI 脚本)
- 前端 NextAuth 时代遗留的
users/accounts/sessions表 - Chat/Message 聊天历史
这次重构把它们逐个剥离。
Neon Cloud (AWS Sydney, 免费 tier)
│
├──◀── Spring Boot backend(pooler 直连)
├──◀── Vercel Next.js Prisma 运行时(prisma.chat.upsert 等)
└──◀── CI 脚本(backfill / leaderboard / test)
凭据管理:每个 .env 手工维护,prod/dev 两份漂移
/home/ubuntu/involution-hell/ 宿主机
│
├── postgres:18-alpine 容器(业务主库 involution_hell + Infisical 专用 infisical 库)
│ │
│ ├──◀── Spring Boot backend 容器(内网 postgres:5432)
│ └──◀── Infisical 容器
│
├── pg-backup 容器(每天 03:00 pg_dump -Fc 到 pg-backups 卷,保留 30d/8w/12m)
│
├── pgAdmin 容器(127.0.0.1:8082,SERVER_MODE=False)
│ ▲ Caddy forward_auth /api/admin/devtool-check
│ │
│ └── api.involutionhell.com/admin/pgadmin/* ←── admin 专属
│
├── Infisical 容器(127.0.0.1:8090)
│ ▲ Caddy 直通反代(不套 gate)
│ │
│ └── secrets.involutionhell.com ←── 所有登录 dev 都能访问(RBAC 在 Infisical 内部)
│
└── Caddy(global-caddy-gateway, host 网络)
终端 TLS, ACME, forward_auth, 按子域路由
Vercel Next.js frontend
│ onFinish 不再 prisma.chat.upsert,改 fetch BACKEND_URL/api/chat/sessions/save
│ 登录时把 satoken 同步写 Domain=.involutionhell.com cookie(跨子域共享)
│ /admin/database 按钮 target=_blank 开 pgAdmin(不 iframe)
│ /u/[username] 增加"密钥管理"按钮(owner 可见,开 Infisical)
-
pg_dump -Fc从 Neon pooler 拉全库(14 张表、5487 行、264 KB 压缩) - 先导入临时
involution_hell_test对行数、约束、索引做 diff 确认 parity - 停 backend → 再 dump 一次(cutover)→ drop + restore 到本地
involution_hell -
.envPGHOST从 Neon endpoint 改为postgres/127.0.0.1(prod / dev 分别) - 重建 backend 容器,
/api/events返回 4 条真实数据 ✅
踩过三种接入方式才定型:
- iframe 嵌 api.involutionhell.com:pgAdmin 的 CSRF cookie 走 SameSite=Lax, 跨站 iframe POST 不带 cookie,登录永远报 "CSRF session token is missing"
- Next.js rewrite 代到同源:pgAdmin 发绝对 URL 重定向带自己 host, 浏览器跟着跳 localhost:8082 变 ERR_CONNECTION_REFUSED
- 新标签打开(最终方案):pgAdmin 在自己 origin 里管自己的 session, 一切正常
中途还发现 pgAdmin SERVER_MODE=False 对公网完全无认证 —— 扫到路径就能
操作生产 DB。一度需要紧急 Caddy 503 封掉路径。最终方案:
api.involutionhell.com/admin/pgadmin/* {
forward_auth 127.0.0.1:8080 {
uri /api/admin/devtool-check # sa-token + @SaCheckRole("admin")
copy_headers Cookie
}
reverse_proxy 127.0.0.1:8082
}为此新增:
- 后端
AdminInfraController.devtoolCheck(泛化自原pgadminCheck),@SaCheckRole("admin"),空响应体,只靠状态码说话 - 前端
lib/use-auth.tsx的syncTokenCookie:登录成功 / 每次刷新时把localStorage.satoken复制一份到Domain=.involutionhell.com的 cookie, 浏览器跨子域自动带。localhost dev 不设 Domain - Caddy 剥 pgAdmin 默认的 X-Frame-Options: DENY,换成 CSP frame-ancestors
Cookie 同步这一步是关键——否则用户直连 api.involutionhell.com/admin/pgadmin/
浏览器发不出 satoken header,forward_auth 永远 401。
原本 app/api/chat/route.ts 的 streamText.onFinish 直连 prisma
写 Chat + Message,Neon 换了之后前端 Prisma 还指向 Neon → 数据写旧库,
和后端读自建 PG 分叉。
方案 A 落地(对比 B: 前端连自建 PG via Cloudflare Tunnel,C: 保持现状双写):
- 后端加
POST /api/chat/sessions/save,body{chatId, userMessage, assistantMessage} -
@Transactional原子写 chat upsert(ON CONFLICT COALESCE userId) + 两条 message - 前端 onFinish 一次
fetch搞定,Vercel AI SDK 的streamText/toUIMessageStreamResponse一字没改,UX 无感 -
scripts/下 3 个 Prisma 脚本(test / backfill / leaderboard)暂未迁移,P2
迁移期间查 user_accounts 表时发现:
-
longsizhuoid=177 roles=admin,usergithub_id=NULL(孤儿) -
github_114939201id=46 roles=usergithub_id=114939201(OAuth 实际命中)
OAuth 登录 AuthService.loginByGithub 按 findByUsername("github_<id>") 匹配,
永远找不到人类 username 的行。之前手工 insert 挂 admin 的管理员全变成孤儿。
热修:merge id=177 的 admin 角色到 id=46,删孤儿。
根治(待做):AuthService 加 findByGithubId fallback,防止再次踩坑。
Mira190 / Crokily 还有同样问题,需要他们的 GitHub 数字 ID 才能修。
历史遗留 users / accounts / sessions / verification_token 四张表
(NextAuth 2026-03-25 退役前写的 85/85/121/0 行)当前无业务挂钩(Chat.userId
全 NULL),选项 A/B/C 处理方案待决。
/home/ubuntu/infisical/ 新建 compose:
-
infisical/infisical:v0.120.0-postgres镜像 - 复用 involution-postgres 容器单建
infisical库 +infisicalsuperuser - 独立
infisical-redis容器(不复用 umami-redis-1,后者会被 flushdb 清空) - 只绑
127.0.0.1:8090,公网由 Caddy 反代
Caddy secrets.involutionhell.com:
- 不套 forward_auth(与 pgAdmin 刻意不同)
- Infisical 自带 GitHub OAuth + project-level RBAC + 审计 log
- 面向所有协作者开放登录页,权限在 Infisical 内部细分
bootstrap .env(/home/ubuntu/infisical/.env,极简 4 项):
-
ENCRYPTION_KEY(32 字符 hex,丢了所有业务 secrets 永久解不开,须离线备份) -
AUTH_SECRET(JWT 签名) DB_CONNECTION_URIREDIS_URL
开发 .env 里历史累积的前端专属变量(NEXT_PUBLIC_* / R2_* / VERCEL_OIDC_TOKEN /
STACK_* / DATABASE_URL / POSTGRES_URL* / INDEXNOW_* / INTERN_KEY /
NEON_PROJECT_ID / AUTH_SECRET)全部清出,它们归前端 Vercel 环境变量管,后端根本不读。
同时:
- prod OPENAI_* 从
sk-+gpt-4.1切到 GLM(open.bigmodel.cn/api/paas/v4+glm-4.6v-flash)。 代码本来就是 OpenAI 兼容协议,URL + Key + Model 三件套指哪打哪,切厂商不改代码 -
PGADMIN_PASSWORD换成openssl rand -base64 24强随机值(虽然 SERVER_MODE=False 时没人用这个密码登录,但镜像启动硬要求存在) - 删掉
CADDY_HTTP_PORT/CADDY_HTTPS_PORT这俩 dead vars - 删掉 NextAuth 遗留的
AUTH_SECRET
最终 prod vs dev .env 的差异只剩必要的部署差异:
| 差异项 | prod | dev |
|---|---|---|
| PGHOST |
postgres(Docker DNS) |
127.0.0.1(宿主机走端口映射) |
| SERVER_PORT | 8080 | 8081 |
| AUTH_URL | https://involutionhell.com |
http://localhost:3010 |
| AUTH_GITHUB_*_DEV | — | 开发用 OAuth App |
| PGADMIN_* / POSTGRES_* | 存在 | 不需要(dev 不跑 compose) |
最后一个设计澄清:pgAdmin 和 Infisical 访问模型不同——
| 工具 | 入口位置 | 公网暴露 | 内部 auth |
|---|---|---|---|
| pgAdmin |
/admin admin 卡片(admin-only 可见) |
有 forward_auth | SERVER_MODE=False 无登录 |
| Umami | 同上 | 有 forward_auth(你自己处理) | 本地账号 |
| Infisical | 个人主页 /u/[username] 的"开发者选项"块(owner 可见) |
直通反代 | GitHub OAuth + project RBAC |
新加 DeveloperToolsIfOwner 组件和 AdminLinkIfOwnerAdmin 并列但放宽条件——
任何本人访问自己主页都能看见"密钥管理 ↗"按钮。
-
Caddy 挂载 Caddyfile 要 restart 不能只 reload:Edit 工具做的是 atomic rename,
inode 变了之后 bind mount 不自动换到新 inode,
caddy reload读的是已 staled 的文件描述符。要么直接 edit in-place 不换 inode,要么docker restart - pgpass 文件必须 UID 5050 + 0600:pgAdmin 容器用户 UID=5050, 主机 pgpass 所有权不对会 restart loop,日志 "can't open /pgpass: Permission denied"
-
Neon 临时 endpoint 不能做公网 DNS:
ep-xxx.neon.tech是 Neon 管理的, 迁出后这些 hostname 立即失效,.env要一次性改完 -
git 仓库本地残留 CI token header:
git config http.extraheader里留着 过期ghs_token 会盖掉 URL 内联的 PAT,git push持续 401 但诊断很难。git config --local --unset-all http.https://github.com/.extraheader清掉 - pgAdmin 的 SERVER_MODE=False 公网暴露是 critical 漏洞:任何扫到路径 的人都能对生产 DB 跑 SQL。外层必须有 gate,或者切 SERVER_MODE=True
-
cookie 跨子域必须
Domain=.involutionhell.com而非Domain=involutionhell.com: 前者是通配所有子域共享,后者严格匹配主域+api子域但不含 www 子域。前缀那个点关键
-
AuthService.loginByGithub加findByGithubIdfallback,防孤儿 admin 再现 - Mira190 / Crokily 孤儿 user_accounts 行的 github_id 回填
-
users/accounts/sessions/verification_token四张 NextAuth 遗留表处置决策 -
scripts/下 3 个 Prisma 脚本(test/backfill/leaderboard)迁到自建 PG 或后端 API - 把业务 secrets 从
.env全部导入 Infisical,compose 启动改走infisical run - Neon
involution-hell项目保留 30 天观察期后删除 - Neon
involutionHell(31.39 MB 第二个项目)盘点内容决定去留