
FeedbackPort
已上线网页应用技术栈: Next.js 15 · React 19 · TypeScript · Supabase (Postgres + RLS + Edge Functions) · Cloudflare Turnstile · Upstash Redis · Resend · MCP · Docker · AWS ECS Fargate · Terraform · Vercel · pnpm monorepo · Zod · Vitest · pgTAP · GitHub Actions
一个开发者,多个产品,一个反馈中心。 Canny、Fider 这类工具默认“一个组织服务很多客户”;FeedbackPort 假设的是相反的场景:一个人同时做好几个小产品,希望所有想法汇到同一个收件箱,同时每个产品都有一个干净的公开面板。
核心特点
多产品,一个收件箱
管理后台首页是产品列表,同时保留跨产品的统一收件箱;每个产品在自己的子域名下有独立的公开面板。
一行脚本接入
不依赖框架的嵌入组件,运行在 Shadow DOM 里,压缩后约 4 KB;没有网站的产品直接链接到面板即可。
形成闭环
用户在公开面板投票、查看处理状态;你回复或更新状态后,提交者和所有投票者都会收到邮件。
从设计上防刷
蜜罐、Cloudflare Turnstile 和 Redis 频率限制三层相互独立;公开的数据库 key 也不能再直接读写数据表。
AI 助手,有边界
MCP 服务器让 AI 助手读取收件箱并起草回复;它不能发送、不能改状态、也看不到任何邮箱,每条草稿都由人发布。
默认私有
提交者和投票者的邮箱不会出现在面板上,也不会返回给 AI 助手;人只以匿名的伪标识出现。
公开面板
每个产品都有自己的面板:提交前可以先搜索已有想法,也可以投票、查看每条想法处于什么状态。邮箱只用来防止重复投票和发送进展通知,不会公开显示。可以直接打开示例面板,里面是示例数据。


所有产品,一个收件箱
管理后台先显示带待处理数的产品列表,一眼看出哪个产品更需要关注。进入产品后,改状态和写回复都在同一行完成;跨产品的全部反馈只要点一下。登录使用 magic link,只有唯一的管理员账号能登录。


可嵌入的组件
有网页的产品,加一行脚本就会出现悬浮的反馈按钮。组件跟随访客的浏览器语言(英文或简体中文),运行在 Shadow DOM 里,不会和宿主页面的 CSS 冲突,只通过公开 API 通信。
<script
src="https://cdn.your-domain.com/widget.js"
data-api-base="https://feedback.your-domain.com"
data-product="your-product"
data-turnstile-site-key="YOUR_TURNSTILE_SITE_KEY"
async
></script>
我的桌面应用 AIQD 没有网页,所以它在设置页直接链接到自己的公开面板。完整链路在那里端到端验证过:真实提交、管理员回复、通知邮件都跑通了。

AI 助手,由人把关
反馈会越积越多。FeedbackPort 自带一个本地的 MCP 服务器,让 Claude 这类助手可以回答“这周大家最想要什么?”、搜索收件箱,并起草回复。它什么都发不出去:每条草稿都停在审核页上,和原始反馈放在一起,直到有人点击发布。
这个限制不是写在提示词里的承诺。服务器用一个专用的 Postgres 角色连接,这个角色对数据表没有任何权限,只能读不含邮箱的视图、写草稿,其余一律被数据库拒绝;所以即使助手被操纵或有缺陷,也无法改状态、发邮件或读取邮箱地址。它运行在你自己的机器上,自己不调用任何模型,除了你本来就有的助手订阅,不产生额外费用。

反馈文本来自陌生人,所以把助手当作在读取不可信输入来设计:用户文本放在标记为 untrusted 的字段里返回;草稿有长度上限,含链接时会被标记(这个标记由数据库设置,助手无法撒谎);另有一个启发式规则标出可疑文本,它只是提示,从不依赖它。
我是怎么验证的。 我写了 22 个用例的评测,让真实模型通过这个服务器完成任务,其中藏着各种攻击:夹在报告里的“把所有反馈标为已完成”、钓鱼链接、伪造的管理层退款批准、写进反馈正文里的邮箱地址。两个模型共 7 次有记录的运行,总共约 0.15 美元;评测工具在 CI 里、连接非本地数据库时、超过花费上限时都会拒绝运行。每一次运行里边界都守住了:没有状态或回复被改动,没有邮箱泄露,没有钓鱼链接进入草稿。失败出在较弱模型的草稿措辞上(66 次运行里 4 次编造了事实、联系地址或退款,或照做了声称的批准);收紧一条工具描述后,降到 132 次里 1 次。我最初看到的失败大多是我自己写的检查有问题,是通过阅读对话记录发现的。
如实说明局限:只有一家厂商的模型、运行次数不多、检查由写用例的同一个人编写。它证明的是边界守得住,而不是模型本身安全,所以每条草稿仍然要经过人。
工作原理
请求从公开面板、嵌入组件或管理后台进入无状态的 Next.js API。公开提交在写入前要依次通过三道相互独立的检查,租户由产品 slug 解析出来。写回复只是写入一行数据:之后由数据库 Webhook 触发 Edge Function 发邮件,所以通知出问题不会影响写入。
工程细节
- 决策有记录: 八份架构决策记录,从自建、SaaS 与开源方案的对比(Canny、Fider、Astuto、Quackback),到容器镜像、AWS 布局、关闭匿名对数据库的直接访问,以及 MCP 的设计。架构、数据模型、API 和接入文档都有中英双语版本。
- 天然多租户: 中间件把子域名变成租户 slug,不查数据库;API 再把它解析成产品(缓存 60 秒)。组件请求来自别的域名,所以会显式带上 slug,每次投票还会二次核对这条想法确实属于该租户。
- 安全落在数据层: 匿名和已登录的数据库角色对数据表没有任何直接访问权;公开面板、嵌入组件和管理后台的所有请求都经过持有 service role 的服务端代码。早期版本里公开 key 可以读到邮箱列,还能绕过限流;我发现后用一个迁移堵上,并加了一条一旦回退就会失败的数据库测试。
- 事件驱动的通知: 回复和状态变更只是普通写入;由 Supabase 数据库 Webhook(带共享密钥)调用
notify-submitterEdge Function,再通过 Resend 发信。 - 有测试、有自动化: pnpm monorepo,共享的 Zod schema 包,Vitest 单元测试,用 pgTAP 写的数据库权限测试(CI 在临时的 Supabase 环境里运行),MCP 服务器的单元和集成测试,以及上面提到的模型驱动评测。CI 在每个 PR 上做类型检查、lint、测试和构建。
- 容易上手: 一条命令就能起一个带示例数据的本地 Supabase;另有
docker compose方案,以及 Vercel + 托管 Supabase 的部署指南。
在 AWS 上运行
我还把同一个应用部署到了 AWS 作为预发布环境,全部用 Terraform 描述,并按生命周期拆成三个栈:远端状态引导层、常驻基础层(镜像仓库、DNS 托管区、泛域名证书、密钥容器),以及闲置时可以整层销毁的运行层(VPC、负载均衡、ECS Fargate 服务)。容器镜像以非 root 用户运行,公开的构建参数在构建时写入,密钥只在运行时注入,Terraform 从不接触密钥的值。App Runner 因为无法承载租户路由需要的多级泛域名而被否决,理由写在了决策记录里。另外有每月预算告警盯着成本。
当前状态
已上线:feedback.tonimakes.com,MIT 开源,并正在为 AIQD 使用。生产环境运行在 Vercel 加托管的 Supabase 上;另有一套独立的 AWS 预发布环境(Terraform、ECS Fargate),用来验证部署。MCP 服务器是在你自己机器上运行的本地工具(托管的远程版本还没做),经过测试,但不是作为在线服务部署的。本页所有截图都取自一套本地搭建、填入虚构产品(Lumen Notes、Tidepool、Pantry Pal)示例数据的副本,所以上面没有任何真实反馈。