skip to content
Logo Zero's Blog

Burn0:一个跑在 Cloudflare Workers 上的阅后即焚应用

/ 11 min read

Burn0:一个跑在 Cloudflare Workers 上的阅后即焚应用

有些内容只需要被看一次,或者只应该在一小段时间内存在。比如临时口令、一次性说明、只想发给某个人看的短文本。把它们发到聊天工具里当然方便,但消息会留在多个客户端、云端备份和通知记录里,后面很难确认它到底还存在于哪里。

Burn0 是我为这个场景做的一个小工具:用户写入一段文本,生成一个分享链接;接收方打开后,消息会按预设规则归零。它不是一个完整的协作系统,也不提供公开消息列表,只专注于一件事:让短文本有明确的生命周期。

Burn0 首页截图

我想解决的问题

这个项目一开始的目标很简单:做一个不用注册、部署成本低、可以自托管的阅后即焚页面。它需要满足几个基本条件:

  • 消息不能出现在公开列表里,只能通过分享链接访问。
  • 消息需要有明确的销毁规则,比如按打开次数、按过期时间,或者二者任一条件触发。
  • 后台需要能处理滥用,包括举报、隔离、删除和来源封禁。
  • 部署要足够轻,不依赖一台长期运行的服务器。
  • 数据库里的内容不能以明文形式保存。

所以我最后选了 Cloudflare Workers 这一套:Worker 处理 API 和静态资源,D1 保存结构化数据,Durable Objects 串行处理打开消息时的计数,Cron Trigger 做定期过期和清理。

核心使用流程

用户侧只有三个主要动作:

  1. 输入要分享的文本。
  2. 选择归零方式。
  3. 生成链接并发给接收方。

Burn0 当前支持三种归零方式:

  • 按时间过期。
  • 按打开次数归零。
  • 时间或次数任一条件满足即归零。

接收方打开链接时,系统会先检查消息状态。如果消息仍然有效,就解密并返回正文;如果已经归零、过期、隔离或删除,页面只返回不可读取状态,不再展示原文。

创建消息流程截图

技术结构

项目的结构比较克制:

public/ 前端静态资源
src/worker/index.js Worker 入口和 API 实现
migrations/ D1 数据库结构迁移
scripts/check.mjs 静态检查脚本
wrangler.jsonc Cloudflare Workers 配置

前端没有引入复杂框架,核心逻辑放在 public/app.jspublic/admin.js。服务端入口是一个 Worker 文件,里面包含 API 路由、数据库初始化、消息状态处理、管理后台接口和定时任务。

这种结构的好处是部署和排查都比较直接。Burn0 的功能边界不大,没有必要为了工程形式引入过多层级。后续如果功能继续增长,再拆模块也不迟。

为什么用 Durable Objects 处理打开消息

阅后即焚应用最容易出问题的地方,是“打开消息”这个动作。

如果一条消息限制只能打开一次,而两个请求几乎同时进来,就可能出现并发竞争:两个请求都看到消息还是可读状态,然后都返回了正文。这种问题单靠普通数据库更新不一定直观,尤其在边缘运行环境里更应该谨慎。

Burn0 用 Durable Objects 按消息 ID 做串行处理。打开同一条消息的请求会进入同一个对象实例,由它按顺序读取、解密、更新计数和状态。这样按次数归零的语义更清楚:一次打开就是一次状态转移,不需要在业务代码里堆很多并发补丁。

数据如何保存

消息正文不会以明文写入 D1。创建消息时,Worker 使用环境变量中的内容密钥加密正文,再把密文、IV、密钥版本和状态信息写入数据库。

这里需要明确一点:这不是严格意义上的端到端加密。服务端 Worker 拥有解密能力,因为它需要在接收方打开链接时返回正文。它解决的是“数据库不要裸存明文”的问题,而不是“服务端完全不可见内容”的问题。

D1 里除了消息本身,还会保存消息事件、举报、管理员、封禁来源、清理设置等数据。来源 IP 和 User-Agent 相关信息也会做哈希或加密处理,避免后台展示和封禁逻辑直接依赖明文存储。

数据结构示意图

举报、隔离和管理后台

阅后即焚并不等于可以忽略滥用。只要允许匿名创建内容,就需要给接收方一个处理入口。

Burn0 支持用户举报消息。举报后,消息会进入隔离状态,用户侧不再展示正文。管理员后台可以查看消息详情、处理举报、隔离消息、删除消息,或者封禁来源。

管理入口默认隐藏,需要连续点击左上角 Burn0 标志中的 0 进入登录页。这个设计不是安全边界,真正的权限仍然依赖管理员账号、密码和会话签名;隐藏入口只是减少普通用户误触。

管理后台消息列表

删除和清理策略

Burn0 里“删除”分两层。

第一层是后台删除消息。管理员删除一条消息时,系统会把消息状态改成 deleted,记录删除时间和原因,并清空正文密文和 IV。这样消息记录仍然存在,后台还能看到基本状态,但正文已经不可恢复。

第二层是物理清理。长期运行后,即使消息已经删除、过期或归零,D1 里仍然会积累历史行。为了解决这个问题,我加了自动清理策略:后台可以配置清理间隔和保留天数,Cron 每 10 分钟唤醒 Worker,但真正的物理删除只会在达到配置间隔后执行。

物理清理只处理终态消息:

  • deleted
  • expired
  • burned

清理时会先删除关联举报和消息事件,再删除消息本身,避免留下孤立数据。后台也提供了“立即清理”按钮,用于手动触发一次清理。

清理设置页面

部署方式

Burn0 的部署目标是尽量低维护。推荐方式是把仓库连接到 Cloudflare Workers Builds:

  1. Fork 项目。
  2. 创建 D1 数据库。
  3. 在 Workers 中连接 Git 仓库。
  4. 配置构建命令 npm ci && npm run check
  5. 配置部署命令 npx wrangler deploy
  6. 绑定 D1,添加必要的环境变量和 Secret。

项目开启了 keep_vars,仓库里的 wrangler.jsonc 只保留稳定的非敏感默认值。真正的密钥,比如内容加密密钥、IP 哈希盐、管理员密码、会话签名密钥,都应该放在 Cloudflare Dashboard 的 Secrets 里。

Cloudflare 部署配置

一些取舍

Burn0 没有把自己做成复杂产品。它没有用户系统,没有团队空间,没有公开消息流,也没有复杂的富文本编辑。原因很简单:这些功能会改变产品性质,也会增加新的数据保留问题。

我更希望它保持小而清晰:

  • 消息有生命周期。
  • 内容加密落库。
  • 打开计数可控。
  • 滥用可以处理。
  • 历史数据可以清理。
  • 部署和维护成本低。

当然,它还有可以继续改进的地方。比如更细的清理统计、更完整的密钥轮换流程、更严格的端到端加密模式,以及更完善的自动化测试。但对于一个自用和可自托管的小工具来说,当前版本已经覆盖了最核心的路径。

结语

Burn0 本质上不是在追求“绝对消失”,因为互联网上的任何展示内容都可能被截图、复制或转发。它真正提供的是一种更明确的默认行为:短文本不应该无限期留在系统里,消息应该有状态、有边界、有清理机制。

对我来说,这个项目最有价值的部分不是某个具体功能,而是把“生命周期”当成一等公民来设计。创建、打开、归零、隔离、删除、清理,每个状态都应该有明确含义。这样一个小工具才不会在长期运行后变成另一个没人敢动的历史数据堆。