数据库变更先经主管批准
查询马上执行;每项变更要等主管在手机上看过确切的 SQL 和预计影响行数并批准后,才执行一次。数据库始终留在你自己的服务器上。
让员工、脚本或助手直接查数据库,能省下好几个小时;让他们不经过任何人就改数据,一句“清掉旧购物车”就可能删掉上周的订单。Hibira 在两者之间放了一位主管:查询马上执行,每一项变更都要等主管在手机上看过确切的 SQL 和会影响多少行、点了批准后才执行。你的数据库和连接密码始终留在自己的服务器上。
用户得到什么
- 只读的问题马上有答案,不用等任何人。
- 每项变更都有一个批准页面,主管可以从推送通知或二维码打开:上面有 SQL、预计影响行数、执行计划和指纹,以及“批准”和“拒绝”。
- 主管一做决定,发起人的页面马上显示结果,不用刷新。被拒绝时会附上主管的理由。
- 保证执行的就是被批准的那一条 SQL,而且只执行一次。

用到的功能
- 审批:
POST /apps/APP_ID/approvals(或 MCP server 的request_approval)会创建一个 Hibira 托管的批准页面,并在频道上通知决定结果。 - 私有频道:每个控制台或每次执行一个,例如
private-sqlgate-<random>,approval事件会送到这里,不用轮询。 - 推送通知(可选):把链接送到主管的手机。
- @hibira-io/approvals/sql:Node.js 的
classifySql()、sqlFingerprint()和assertApprovedSql(),Python 也有同样的工具。
可以在 demos.hibira.io 试试:选一项变更,用手机扫二维码,然后点批准。
步骤 1:查询放行,其他一律拦下
classifySql() 只有在确定的时候才把一条 SQL 当成查询:只有一条语句、开头是 SELECT、WITH、EXPLAIN 或 SHOW,而且没有任何会改数据的部分。多条语句、WITH ... DELETE、EXPLAIN ANALYZE、SELECT ... INTO、FOR UPDATE,以及任何无法确定解析的内容,都算变更。查询还是要放在只读事务里执行,这样即使判断错了,也只会失败,不会写入:
import pg from 'pg';
import { classifySql } from '@hibira-io/approvals/sql';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
export async function runSql(sql, requestedBy) {
if (classifySql(sql) === 'read') return { rows: await readOnly(sql) };
return { approval: await askSupervisor(sql, requestedBy) };
}
async function readOnly(sql) {
const db = await pool.connect();
try {
await db.query('BEGIN READ ONLY');
const { rows } = await db.query(sql);
await db.query('COMMIT');
return rows.slice(0, 200);
} catch (err) {
await db.query('ROLLBACK');
throw err;
} finally {
db.release();
}
}步骤 2:让主管看到这项变更会做什么
提交前先估算行数。EXPLAIN 完全不会执行;在会回滚的事务里试运行能拿到精确的行数,但语句确实会跑一次,所以要设超时。再对 SQL 原文算出指纹:这次批准只对这一条 SQL 有效。
import { sqlFingerprint } from '@hibira-io/approvals/sql';
async function preview(sql) {
const db = await pool.connect();
try {
await db.query('BEGIN');
await db.query("SET LOCAL statement_timeout = '5s'");
const { rowCount } = await db.query(sql);
return { rows: rowCount, fingerprint: await sqlFingerprint(sql) };
} finally {
await db.query('ROLLBACK');
db.release();
}
}步骤 3:提交批准
创建审批时,把 SQL 和预览放在 details,并指定要通知结果的频道。SQL 和指纹留在你这边,用审批 ID 对应。发到 Hibira 的只有审批的文字:没有连接密码,也没有任何数据行。
import Pusher from 'pusher';
const hibira = new Pusher({ appId: process.env.HIBIRA_APP_ID, key: process.env.HIBIRA_KEY, secret: process.env.HIBIRA_SECRET, host: 'ws.hibira.io', useTLS: true });
async function askSupervisor(sql, requestedBy) {
const { rows, fingerprint } = await preview(sql);
const res = await hibira.post({
path: '/approvals',
body: {
title: 'Run this database change?',
details: `${sql}\n\nEstimated rows: ${rows}\n\nSHA-256: ${fingerprint}`,
options: ['Approve', 'Reject'],
channel: `private-sqlgate-${requestedBy.consoleId}`,
push_users: ['supervisor-42'],
expires_in_minutes: 15,
agent: 'SQL console',
},
});
const approval = await res.json();
await pool.query('INSERT INTO pending_changes (approval_id, sql, fingerprint) VALUES ($1, $2, $3)', [approval.approval_id, sql, fingerprint]);
return { id: approval.approval_id, url: approval.url, rows };
}主管会收到带链接的推送通知(或直接扫发起人页面上的二维码),在以自己语言显示的页面上回复。

步骤 4:结果一到,只执行一次
决定会以 approval 事件送到频道上。把它当成门铃:先从 API 读一次结果,确认存下来的 SQL 和指纹仍然一致,再执行。先“认领”这一行,就算两个事件或两个 worker 同时进来,也只会执行一次:
import { assertApprovedSql } from '@hibira-io/approvals/sql';
// In the requester's page (pusher-js), or a worker that keeps a connection open:
channel.bind('approval', ({ id, reason }) => {
if (reason === 'decided') fetch(`/api/changes/${id}/run`, { method: 'POST' });
});
// POST /api/changes/:id/run on your server:
export async function runApproved(id) {
const res = await hibira.get({ path: `/approvals/${id}` });
const answer = await res.json();
if (answer.status !== 'approved') return { status: answer.status, comment: answer.comment };
const { rows } = await pool.query('UPDATE pending_changes SET ran_at = now() WHERE approval_id = $1 AND ran_at IS NULL RETURNING sql, fingerprint', [id]);
if (!rows.length) return { status: 'already run' };
await assertApprovedSql(rows[0].sql, rows[0].fingerprint);
const result = await pool.query(rows[0].sql);
return { status: 'ran', rows: result.rowCount };
}
已经用 MCP server 连数据库了?
如果团队是在 Claude、Cursor、ChatGPT 或其他 MCP 客户端里,通过 DBHub、postgres-mcp、Supabase MCP 这类 server 操作数据库,只要在前面加上 mcp-approval-gate,一行代码都不用改。查询直接通过;每一次插入、更新、删除或结构变更,都会停在同一个审批页,主管批准后才原样执行一次。
npx -y @hibira-io/mcp-approval-gate --label shop-production --push-interests db-approvals \
-- npx -y @bytebase/dbhub --transport stdio --dsn "$DATABASE_URL"在你用的工具里做同样的把关
- Vercel AI SDK、Mastra、LangGraph(JavaScript 和 Python):@hibira-io/approvals 每一种都有示例:把
needsApproval或requireApproval写成按 SQL 判断的函数,并在你自己的应用里用审批收件箱显示每个调用的预览。 - Laravel:filament-approvals 直接在 Filament 后台里审批,README 里有给 Laravel AI SDK 用的
RunSql工具。 - n8n、Make、Activepieces 和 Dify:用代码步骤判断 SQL 类型,Hibira 的审批步骤请主管决定并等待结果,数据库步骤只接在“批准”的分支上。每个的 README 都有完整流程。
- Claude、Cursor 和其他 MCP 客户端:MCP server 的
request_approval也能在对话中用同样的方式提交审批。 - 任何数据库 MCP server:mcp-approval-gate 不用改代码,就能包住 DBHub、postgres-mcp、Supabase MCP 等 server。