PocketBase 做一个本地低代码后台:手机表单和文件上传跑通后,用 cpolar 给产品临时验收
PocketBase 做一个本地低代码后台:手机表单和文件上传跑通后,用 cpolar 给产品临时验收
有些 Demo 不值得起一套 Spring Boot + Vue,也不想为了一个临时表单去申请 SaaS。我的做法更粗暴:PocketBase 单文件启动,本机把 collection、字段、上传和权限跑通,再用 cpolar 给产品一个临时 HTTPS 地址,让他直接拿手机验收。
这篇只做一件事:把 PocketBase 当成一个本地低代码后台,跑通新增、查询、文件上传和基础权限。验收结束就关隧道,不把本机长期挂到公网。
这类场景,PocketBase 很合适
PocketBase 本质上是一个单文件后端:自带 SQLite、Admin UI、Auth、Collection、REST API、文件存储。它不像 NocoDB 那样主打表格协作,也不是 Appsmith 那种内部工具面板,更不是 Directus CMS。
我最喜欢它的一点是:本地原型特别快。比如要给产品验收一个“线索收集表”,字段里有姓名、手机号、备注、附件截图。正常写后端要建项目、连库、写接口、处理上传、配权限;PocketBase 里点几个字段,接口和上传就出来了。
这篇用 macOS/Linux 命令演示,Windows 也一样,下载对应压缩包后执行pocketbase.exe serve即可。端口使用 PocketBase 默认的8090,cpolar 只转发这个端口。
下载并启动 PocketBase
先新建一个演示目录,避免把它和别的项目混在一起:
mkdir -p ~/demo/pb-feedback cd ~/demo/pb-feedback到 PocketBase 官网下载对应系统的 release 包。macOS Apple Silicon 可以用类似下面的方式,版本号按官网最新稳定版替换:
curl -L -o pocketbase.zip \ https://github.com/pocketbase/pocketbase/releases/download/v0.23.12/pocketbase_0.23.12_darwin_arm64.zip unzip pocketbase.zip chmod +x pocketbaseLinux x64 可以换成:
curl -L -o pocketbase.zip \ https://github.com/pocketbase/pocketbase/releases/download/v0.23.12/pocketbase_0.23.12_linux_amd64.zip unzip pocketbase.zip chmod +x pocketbase启动服务:
./pocketbase serve --http=127.0.0.1:8090我这里故意绑定127.0.0.1,意思是只允许本机访问。后面要给外部手机验收,也通过 cpolar 转发这个本地端口,而不是让 PocketBase 直接监听0.0.0.0。
启动后终端会看到类似地址:
Server started at http://127.0.0.1:8090 REST API: http://127.0.0.1:8090/api/ Admin UI: http://127.0.0.1:8090/_/浏览器打开:
http://127.0.0.1:8090/_/第一次进入会要求创建 Admin。这里用一个只给本次演示使用的邮箱和强密码,别用生产密码,也别把密码写进聊天群。
建一个 feedback collection
进入 Admin 后,左侧选择 Collections,新建一个feedbackcollection。类型选 Base collection 就够了,它适合公开表单、匿名反馈、测试数据这类场景。
字段我建议这样建:
| 字段名 | 类型 | 说明 |
|---|---|---|
| name | Text | 提交人姓名,设为必填 |
| phone | Text | 手机号或测试联系方式,演示环境用假数据 |
| message | Text | 反馈内容,设为必填 |
| attachment | File | 上传截图或附件,允许 1 个文件 |
| status | Select | 状态,选项为 new、reviewed,默认 new |
attachment字段可以限制文件类型,比如只允许:
image/png,image/jpeg,application/pdf文件大小也建议限制一下,例如 5MB。Demo 阶段别让别人随手传几十 MB 的视频,把本机磁盘打满。
status字段用于后续内部处理,不建议让手机表单提交时随便改。我们可以在前端不传它,PocketBase 使用默认值new。
配基础权限,别一上来全放开
PocketBase 每个 collection 都有 API Rules。为了让产品手机端能匿名新增和查询演示数据,可以先配成下面这样:
| 规则 | 配置 |
|---|---|
| List/Search rule | 空字符串 |
| View rule | 空字符串 |
| Create rule | 空字符串 |
| Update rule | @request.auth.id != "" |
| Delete rule | @request.auth.id != "" |
这里的“空字符串”不是随便写一个空格,而是规则输入框留空。它表示允许公开访问该动作。
这样配置后,未登录用户可以新增、列表查询、查看详情;但不能更新和删除。对临时验收来说,这个边界比较清楚:产品能在手机上提交、刷新看到结果,但不会误删数据。
如果你的验收数据不允许公开查询,那就把 List/View 也改成需要登录,或者单独建一个测试账号。本文为了让手机验收链路最短,采用公开读写新增的 Demo 配置,但只放测试数据。
血泪教训:不要为了省事把所有规则都留空。Create 可以开放,Update/Delete 不要开放。否则别人拿到地址后可以直接改掉或删掉你的测试记录。
用 REST API 先跑通新增
在做 H5 页面前,先用 curl 验证 API。新增一条不带文件的数据:
curl -X POST http://127.0.0.1:8090/api/collections/feedback/records \ -H 'Content-Type: application/json' \ -d '{ "name": "测试用户A", "phone": "13800000000", "message": "手机端表单基础字段验收", "status": "new" }'返回里会有id、created、updated等字段。再查列表:
curl 'http://127.0.0.1:8090/api/collections/feedback/records?sort=-created'如果能看到刚才的数据,collection 和基础权限就通了。
再试一下文件上传。准备一张测试图,比如test.png,然后执行:
curl -X POST http://127.0.0.1:8090/api/collections/feedback/records \ -F 'name=测试用户B' \ -F 'phone=13900000000' \ -F 'message=带附件上传的反馈' \ -F 'status=new' \ -F 'attachment=@./test.png'PocketBase 的文件字段走multipart/form-data。上传成功后,记录里会出现attachment文件名。文件访问路径一般是这种格式:
/api/files/feedback/{recordId}/{filename}在 Admin UI 里点开这条记录,也能看到上传文件。这一步很关键,别跳过。很多 Demo 现场翻车,不是接口没通,而是文件字段类型、大小限制或权限没提前测。
写一个最小 H5 表单
只用 API 验收不够,产品一般还是希望拿手机点一下。所以我们写一个最小index.html,直接放在本地目录里打开即可。
新建文件:
cat > index.html <<'EOF' <!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>PocketBase 手机反馈验收</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 20px; max-width: 680px; margin: auto; } label { display: block; margin-top: 14px; font-weight: 600; } input, textarea, button { width: 100%; box-sizing: border-box; margin-top: 6px; padding: 10px; font-size: 16px; } textarea { min-height: 100px; } button { margin-top: 18px; border: 0; border-radius: 8px; background: #111827; color: white; } .item { border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin-top: 12px; } .muted { color: #666; font-size: 13px; } </style> </head> <body> <h2>PocketBase 反馈表单验收</h2> <form id="form"> <label>姓名</label> <input name="name" required placeholder="测试用户A" /> <label>手机号</label> <input name="phone" placeholder="13800000000" /> <label>反馈内容</label> <textarea name="message" required placeholder="这里填写测试反馈"></textarea> <label>附件截图</label> <input name="attachment" type="file" accept="image/png,image/jpeg,application/pdf" /> <button type="submit">提交反馈</button> </form> <h3>最新提交</h3> <div id="list" class="muted">加载中...</div> <script> const PB_BASE = location.origin.includes('8090') ? 'http://127.0.0.1:8090' : location.origin; async function loadList() { const res = await fetch(`${PB_BASE}/api/collections/feedback/records?sort=-created&perPage=10`); const data = await res.json(); const list = document.querySelector('#list'); list.innerHTML = ''; for (const row of data.items || []) { const fileUrl = row.attachment ? `${PB_BASE}/api/files/feedback/${row.id}/${row.attachment}` : ''; const div = document.createElement('div'); div.className = 'item'; div.innerHTML = ` <strong>${row.name || '匿名'}</strong> <div>${row.message || ''}</div> <div class="muted">${row.phone || ''} | ${row.created}</div> ${fileUrl ? `<div><a href="${fileUrl}" target="_blank">查看附件</a></div>` : ''} `; list.appendChild(div); } } document.querySelector('#form').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('status', 'new'); const res = await fetch(`${PB_BASE}/api/collections/feedback/records`, { method: 'POST', body: fd }); if (!res.ok) { alert('提交失败:' + await res.text()); return; } e.target.reset(); await loadList(); alert('提交成功'); }); loadList(); </script> </body> </html> EOF这里有个小设计:如果页面是通过http://127.0.0.1:8090或 cpolar 入口访问,就让 API 走同源地址。这样手机打开 cpolar 的 HTTPS 地址时,请求也会打到同一个 PocketBase 服务,不需要额外配置前端服务器。
要让 PocketBase 托管这个 HTML,可以把它放进pb_public目录:
mkdir -p pb_public mv index.html pb_public/index.htmlPocketBase 会把pb_public作为静态目录。现在本机打开:
http://127.0.0.1:8090/你应该能看到表单。提交一条带图片的数据,再刷新列表,能看到新增记录和附件链接。
用 cpolar 开一个临时 HTTPS 入口
本地链路通了,再给产品或测试同事一个手机可访问地址。安装并登录 cpolar 后,执行:
cpolar http 8090终端会输出一个 HTTPS 地址,形如:
https://xxxx.cpolar.top把这个地址发给产品,让他用手机浏览器打开:
https://xxxx.cpolar.top/验收清单建议直接写清楚,别让对方自由发挥:
- 打开手机表单页面;
- 新增一条反馈;
- 上传一张截图或 PDF;
- 刷新后能看到最新记录;
- 点开附件可以预览或下载;
- 尝试修改/删除记录应该不允许;
- Admin UI 只由开发本人本机使用,不给外部人员账号。
如果产品要看后台记录,建议你共享屏幕或导出测试数据,不要把 Admin 账号直接发出去。临时验收看的是链路,不是把管理权限交出去。
验收时最容易忽略的权限问题
我建议你专门测一下 Update/Delete。PocketBase 的 REST API 路径很直白,别人拿到记录 ID 后可以尝试请求:
curl -X PATCH http://127.0.0.1:8090/api/collections/feedback/records/RECORD_ID \ -H 'Content-Type: application/json' \ -d '{"message":"我试图修改这条记录"}'如果规则配置正确,未登录请求会被拒绝。删除也是一样:
curl -X DELETE http://127.0.0.1:8090/api/collections/feedback/records/RECORD_ID这一步不是找麻烦,而是防止“链接发出去之后,测试数据被别人改掉”。Demo 可以简单,但权限边界不能糊。
如果你需要让产品登录后才能查看列表,可以用 PocketBase 的 Auth collection 建一个测试用户,再把 List/View rule 改成:
@request.auth.id != ""对应前端再加登录接口。本文为了控制篇幅,不展开完整登录页。真实项目里,我更推荐把查询和管理动作收回到登录态下,只把 Create 暂时开放。
安全边界要提前说清楚
这类本地临时验收最怕两件事:一是把生产数据拿来演示,二是把本机不该暴露的东西顺手暴露出去。
我的底线是下面这些:
- 只使用测试数据,不放真实手机号、客户资料、合同、身份证、内部截图;
- cpolar 只转发
8090这个 PocketBase Web/API 端口; - 不暴露宿主机目录、SQLite 数据库文件、
pb_data、服务器 shell; - 不暴露 Admin 密码,不把 Admin 账号发给外部验收人员;
- 不开放 MySQL、Redis、SSH、Docker 面板等其他开发端口;
- 验收结束马上关闭 cpolar 进程;
- 长期环境要改成正式鉴权、正式域名、HTTPS 证书、权限策略、备份和日志审计。
PocketBase 的数据默认在当前目录的pb_data里。这个目录不要拿来做静态文件目录,也不要通过其他文件服务暴露出去。SQLite 文件不是给别人下载的,Admin 密码也不是验收材料。
关闭临时入口很简单,在运行 cpolar 的终端按:
Ctrl + CPocketBase 也可以按Ctrl + C停掉。验收结束后,如果不再需要这批数据,可以整个演示目录打包归档或删除。
写在最后
PocketBase 适合解决一种很具体的问题:我现在就需要一个能新增、能查、能上传文件、能用 REST API 对接的小后端,而且希望今天就给产品在手机上验收。
它不是万能低代码平台,也不应该被拿来绕过正式后端设计。但在原型、内测、表单、文件上传 Demo 这些场景里,单文件启动、Admin UI 配 collection、REST API 直接可用,确实很省时间。
我的建议是:本地开发大胆用,临时验收短时开,生产上线认真收口。只要边界守住,PocketBase + cpolar 这个组合会非常顺手。
