028开发工具阅读记录

从 SaaS 到自托管:Requestly 的用法、改造过程与结果

Requestly 从 SaaS 改造为 PostgreSQL 自托管服务
第 028 期

Requestly 是一款开源 HTTP 拦截与 Mock 工具。它最实用的地方,是让开发者不改业务代码、不另起一套代理服务,就能在浏览器里重定向请求、修改 Header、覆盖接口响应、注入脚本,或者模拟延迟和异常。

这类工具很适合个人调试,但进入内部团队场景后,问题会从“怎样改请求”变成“规则怎样共享、账号怎样管理、数据放在哪里”。官方项目的 Web App 与账号、Firebase、邀请和订阅体系结合得比较深,直接把源码部署起来,并不等于得到了一套真正独立的私有服务。

因此我基于 Requestly 做了一次自托管改造:用 PostgreSQL 保存团队账号和规则,以本地 Fastify 服务签发 JWT,增加超级管理员与团队 RBAC,再让浏览器插件可以指向任意自托管地址。本文记录 Requestly 的基本用法,也复盘这次改造为什么发生、方案怎样几次转向,以及最终得到了什么。

实现基于 Requestly v26.5.22-pre-restructure,完成于 2026 年 8 月。代码在 citrusjunoss/requestlycodex/postgres-self-hosted 分支。

Preview
Requestly 自托管改造封面

Requestly 能解决什么问题

Requestly 的核心不是“抓包”,而是在请求真正到达目标之前改变它。浏览器插件适合处理当前浏览器里的流量,桌面端则可以覆盖浏览器、移动设备和其他桌面应用。

常见使用方式包括:

  • 把生产或测试环境的某个接口重定向到本地服务。
  • 将线上 JavaScript、CSS 替换为本地文件,快速验证修复。
  • 添加、删除或覆盖请求与响应 Header,例如调试 CORS、鉴权和缓存。
  • 修改请求参数、请求体或响应体,模拟后端尚未完成的字段。
  • 直接返回 Mock 数据,覆盖成功、空数据、异常和边界场景。
  • 注入 JavaScript 或 CSS,验证页面行为和样式。
  • 增加网络延迟或失败响应,观察 loading、超时和重试逻辑。

一个典型场景是:页面原本请求 https://api.example.com/users,本地正在开发新接口,希望暂时改到 http://localhost:8080/users。在 Requestly 中创建 Redirect Rule,设置来源 URL、目标 URL 和匹配条件,启用规则并刷新页面即可。业务仓库不需要提交临时地址,也不必修改其他人的环境。

规则通常按下面的步骤使用:

  1. 安装浏览器插件,打开 Requestly Web App。
  2. 根据目标选择 Redirect、Modify Headers、Modify Response、Mock 或 Delay 等规则。
  3. 设置 URL 匹配范围,尽量限定域名、路径和请求方法,避免误伤其他请求。
  4. 填写替换内容或 Mock 响应,保存并启用规则。
  5. 在浏览器 Network 面板确认规则是否命中,再完成异常、回退和关闭规则的验证。

规则可以分组、开关和共享,这也是它比浏览器 DevTools 临时 Override 更适合长期开发流程的地方。

为什么要改造成自托管

这次需求并不是把 Requestly 改成只能在一台电脑运行的“纯本地版”,而是搭建一套部署在自有服务器上、可以被团队多设备访问的私有服务。

目标很明确:

  • 保留团队、成员和 admin/write/read 权限。
  • 使用团队标识、用户名和密码登录,不要求邮箱。
  • 不需要注册、邀请邮件、域名邀请、Billing 和订阅流程。
  • 规则存储在自己的数据库中,可以跨设备同步。
  • 超级管理员负责创建团队和首个团队管理员。
  • 浏览器插件不再固定打开官方 Web 地址。
  • 自托管模式不受 SaaS 套餐和 Premium 弹窗阻断。

这背后有三个实际原因。

第一,内部账号不需要完整的互联网用户体系。邮箱验证、忘记密码邮件、公开邀请链接和计费席位在 SaaS 产品里合理,在一个管理员创建账号的小团队里反而增加部署和维护成本。

第二,规则本身可能包含内网域名、测试接口、临时鉴权 Header 和 Mock 数据。把团队账号、规则和同步事件放在自有 PostgreSQL 中,数据边界更清晰,也便于备份和审计。

第三,仅仅关闭登录弹窗并不能得到团队版 Requestly。现有同步路径、工作区成员、RBAC 和前端全局状态都依赖稳定的用户标识;没有身份就无法回答“谁能读、谁能写、禁用谁的账号”。真正要移除的是官方账号和商业化流程,而不是身份与权限本身。

第一次判断:权益容易旁路,账号不能直接删除

最初的代码分析给出了一个很重要的结论:权益校验虽然分散在许多页面,但核心逻辑集中在 featureLimiterPremiumFeature 一类入口,适合增加正式的自托管开关;账号状态的影响面则大得多。

登录初始化除了显示用户信息,还会设置同步身份、工作区、订阅状态和存储路径。规则同步依赖 uid,团队数据又依赖 teamId + member role。因此“彻底删除账户代码”会同时破坏云同步、工作区切换和权限控制。

访问码也不能替代用户。多人共用一个访问码时,系统无法区分成员,无法单独禁用账号、调整角色或撤销会话。最终采用的是轻量团队账号:

team slug + username + password -> stable account id -> team role

它去掉了邮箱和公开注册,却保留了私有协作真正需要的身份边界。

第一版方案:本地账号服务仍然桥接 Firebase

第一版实现选择了改动较小的路线:在仓库中新增 Node.js/Fastify 服务,保存账号密码并通过 Firebase Admin 签发 Custom Token。前端使用团队标识、用户名和密码登录,成功后仍进入 Firebase Auth;现有 Firestore 团队结构与 Realtime Database 规则同步可以继续复用。

这版很快跑通了几个关键流程:

  • 超级管理员通过环境变量登录。
  • 创建团队时同时创建首个团队管理员。
  • 团队管理员创建成员并分配角色。
  • 前端切换到账号密码登录页。
  • 自托管开关关闭 Pricing、Billing、Invite 和权益阻断入口。

但它暴露了一个概念偏差:本地 Fastify 只是替代了 Cloud Functions,Firebase Auth、Firestore 和 Realtime Database 仍然负责身份与数据。它能在本机启动,却不是完全自托管。

这个阶段也让我重新区分了两个词:

  • 本地运行:Web 和 API 在自己的电脑或服务器上启动。
  • 自托管:认证、数据和核心业务链路都由自己控制,不依赖官方云服务。

把第一版部署到服务器,仍然需要 Firebase 凭据,也无法满足数据完全落在私有环境的目标。

第二次转向:统一迁移到 PostgreSQL

另一个关键问题是规则同步。如果只把账号改成本地 JWT,规则仍保存在每台客户端,那么服务器上的“团队”只有成员表,没有共享内容。两台设备登录同一个团队也看不到同一组规则。

因此最终方案不再使用 SQLite + 客户端本地规则,也不继续用 Firebase Custom Token,而是统一使用 PostgreSQL 16:

  • teams 保存团队与唯一 slug。
  • accounts 保存团队账号、密码 Hash、角色和禁用状态。
  • refresh_sessions 保存 Refresh Token Hash 与撤销信息。
  • records 保存团队共享规则及版本号。
  • user_rule_configs 保存成员个人的规则启用状态等配置。
  • sync_events 保存单调递增游标和变更记录。
Preview
Requestly 自托管最终架构

后端继续使用 TypeScript 和 Fastify,通过 Drizzle 管理 schema 与 migration。密码使用 Node.js crypto.scrypt,超级管理员和团队用户使用不同的 JWT 密钥。Access Token 默认 15 分钟,Refresh Token 默认 7 天并放在 HttpOnly Cookie 中;数据库只保存 Refresh Token Hash。

规则同步采用 snapshot、changes 和 batch 三类接口。客户端首次进入工作区获取快照,之后按 cursor 拉取增量事件;写入携带版本号,冲突返回 409。这样不必让前端直接访问 PostgreSQL,也不需要在第一版引入 WebSocket。

保留前端外壳,只替换底层传输

Requestly 的规则编辑器本身已经比较成熟,重写它既没有必要,也会显著扩大风险。改造中的重要取舍,是保留原有存储接口和大部分页面,把内部 Firebase transport 换成本地同步适配器。

自托管构建由三个变量控制:

VITE_BACKEND_BASE_URL=http://localhost:8090
VITE_PASSWORD_AUTH_ENABLED=true
VITE_DISABLE_ENTITLEMENTS=true

启用后:

  • /signin 展示团队标识、用户名和密码。
  • /admin/signin 提供超级管理员登录,/admin 提供团队管理页面。
  • 规则读写通过本地适配器转换为 snapshot、changes 和 batch API。
  • 团队管理员可以创建账号、修改角色、重置密码、禁用或移除成员。
  • 邮箱、邀请、计费和订阅不再进入自托管核心流程。
  • 权益检查返回可用,不再用临时硬编码绕过 Premium 弹窗。

这里有一个实际踩过的坑:只修改源码而没有让 Vite 读取正确的 .env.local,页面仍然会走原 SaaS 登录与权益逻辑。另一个问题是旧 Header 的 Sign in 按钮仍尝试打开 Firebase 登录弹窗,在自托管模式下点击后没有反应。最终将它改为直接跳转 /signin,同时隐藏不适用的自助 Sign up

让部署从“能启动”变成可重复

开发阶段先用组合进程按顺序启动 PostgreSQL migration、API 和 Vite。这个过程发现了两个容易误判的问题:

  • PostgreSQL 容器已经启动,不代表数据库已经可以接收连接。
  • process supervisor 显示 Running,不代表 API 和 Web 端口真的已经就绪。

迁移脚本因此增加了最多 60 秒的临时连接错误重试,启动流程则明确等待数据库、执行 migration、检查 API /health,最后才启动前端。

生产和独立安装使用 Docker Compose,包含 PostgreSQL、Fastify API 与 Nginx Web:

./docker-start.sh

首次启动会生成未提交的 .env.docker,其中包含数据库密码、两套 JWT 密钥和超级管理员初始密码。默认只暴露 http://localhost:3000,Nginx 将同源 /api 代理到 Fastify,PostgreSQL 和 API 不映射到宿主机公网端口。

Web 镜像构建需要 Docker 至少分配 4 GiB 内存,建议 6 GiB。低于这个值时,脚本会提前提示,而不是等大型前端构建在 chunk 阶段 OOM。API 容器使用非 root 用户运行,Compose 通过健康检查后才报告启动成功。

首次使用可以按以下顺序完成:

  1. 执行 ./docker-start.sh,保存 .env.docker 中生成的凭据。
  2. 访问 http://localhost:3000/admin/signin,使用超级管理员登录。
  3. 创建团队,填写唯一 slug,并设置首个团队管理员账号密码。
  4. 访问 http://localhost:3000/signin,使用 slug + username + password 登录。
  5. 进入规则页面,创建 Redirect、Header、Response 或 Mock 规则。
  6. 给其他成员创建账号,按职责分配 adminwriteread

公网部署时还需要 HTTPS、SECURE_COOKIES=true、严格的 CORS_ORIGINS,并把 .env.docker、JWT 密钥和数据库备份作为同一套灾备资料保存。发布顺序固定为备份、迁移、API 健康检查、Web 切换和冒烟验证。

浏览器插件也必须认识私有地址

Web App 自托管完成后,浏览器插件仍然可能把 Popup、DevTools 或规则编辑入口指向官方站点。只改构建时常量意味着每换一个域名都要重新打包,也不适合发布同一个内部插件给多个环境。

最终给 MV3 插件增加了 Options 页面,将自托管 Web URL 保存到 chrome.storage.local。地址变化后,Service Worker 会注销旧 App Bridge,为新 origin 注册内容脚本,并重建普通页面脚本的排除规则。Popup、DevTools、规则编辑、录制和测试规则入口统一读取这个地址。

插件构建命令为:

npm --prefix browser-extension/config run build
npm --prefix browser-extension/common run build
npm --prefix browser-extension/mv3 run build:current

然后在浏览器扩展管理页加载 browser-extension/mv3/dist,进入扩展 Options,将地址设置为 http://localhost:3000 或实际 HTTPS 域名,保存后刷新已经打开的 Requestly Web 页面。

当前 Chrome、Edge 和 Firefox 的 MV3 构建已接入运行时地址;Safari 仍使用独立构建入口。静态 Delay DNR 重定向规则也仍使用构建时地址,这是现阶段明确保留的边界。

最终结果

这次改造最终形成了一条完整的私有协作链路:

超级管理员创建团队
  -> 团队管理员创建成员
  -> 成员使用 slug + username + password 登录
  -> Web App 通过 JWT 调用自托管 API
  -> 规则写入 PostgreSQL
  -> 其他设备按 cursor 获取增量变更
  -> 浏览器插件始终回到私有 Web 地址

已经完成的自动验证包括:

  • 服务端单元测试、TypeScript 类型检查和构建。
  • PostgreSQL 空库 migration 与重复 migration。
  • 超级管理员登录、团队账号登录和 API 健康检查。
  • Web 生产构建与 /signin 深链接刷新。
  • Nginx 同源 /api 代理。
  • Docker Compose 配置、健康等待和数据持久化链路。
  • Chrome MV3 插件构建。
  • 自托管登录入口与页面交互的浏览器验证。

代码提交按能力拆成 PostgreSQL/JWT、启动可靠性、登录路由、Docker Compose、管理后台与插件配置、改造文档几个阶段,最终推送到个人 fork。这样既保留了上游仓库作为 upstream,也为后续同步官方更新留出了边界。

仍然没有做的事情

自托管并不意味着仓库中的所有云服务代码都已经物理删除。为了减少对上游源码的破坏,部分官方兼容代码仍保留,但启用自托管开关后,账号、团队和规则同步主链路不会使用它们。

当前边界包括:

  • 不自动迁移旧 Requestly/Firebase 账号和规则数据。
  • 不提供邮箱验证、找回密码、邀请邮件和公开邀请链接。
  • 不提供 Billing、订阅、发票与套餐到期逻辑。
  • Safari 插件尚未接入运行时 Web URL。
  • 插件仍需要在目标浏览器完成 unpacked 端到端验收。
  • 公网部署仍需实际验证 Cookie、CORS、反向代理和备份恢复。
  • 同步冲突已有版本检测,但前端还可以补充更明确的冲突提示与重试体验。

这些不是遗漏,而是自托管 V1 的范围控制。先保证账号、权限、规则同步和部署闭环可靠,再逐步增加审计日志、登录锁定、密码策略、会话管理和旧数据导入,维护成本会更可控。

这次改造最重要的几个判断

第一,移除账号页面不等于移除身份。只要存在私有同步和团队权限,就必须保留稳定账号、会话和授权模型。

第二,本地运行不等于自托管。核心认证或数据仍依赖 Firebase 时,服务器只是换了一个前端和代理入口。

第三,优先替换边界,不要重写成熟业务。保留规则编辑器与存储抽象,只替换认证、同步 transport 和部署方式,显著降低了改造风险。

第四,部署可靠性是产品能力的一部分。数据库 readiness、migration、健康检查、进程清理、Secret 管理和备份恢复,不应该等到上线当天再补。

第五,浏览器插件与 Web App 是同一条产品链路。Web 改成私有地址后,插件里的跳转、内容脚本和 App Bridge 也必须具备运行时配置能力,否则所谓自托管仍然是不完整的。

Requestly 原本已经提供了很强的请求改写能力。这次工作的价值不是重造这些规则,而是把它从一个依赖官方 SaaS 账户的开发工具,变成一套账号、权限、规则和部署都掌握在自己手中的团队服务。

完整实现与部署细节见仓库中的 SELF_HOSTED_REFACTORING_PLAN.md