Approval rules
Approval rules are per-database, table-glob policies that gate
writes at /v1/query, /v1/batch, and /v1/apply. A write that
touches a matching table either gets blocked outright (deny) or
returns a 403 with an approval URL the human accepts (require_approval).
Reads are never gated. For row-level visibility, use Row-Level Security instead.
Add a rule
Section titled “Add a rule”Console → Database → Approval rules. Or programmatically via the SDK (admin-role bearer required):
await db.approvalRules.create({ tableGlob: "production_*", action: "require_approval", note: "All production tables need human sign-off",});Same operations as HTTP — read with db.approvalRules.list(), remove
with db.approvalRules.delete(ruleId). The cookie-session console
endpoints still work for the UI:
POST /api/namespaces/{ns}/databases/{slug}/approval-rulesGlob syntax: * matches any chars, ? matches one. Glob is
matched (case-insensitive) against unqualified table names parsed
out of the SQL.
What an agent sees
Section titled “What an agent sees”When a write hits a require_approval rule the API returns 403 with
the approval shape:
{ "success": false, "error": "Statement requires human approval before it can run", "approvalToken": "appr_…", "approvalUrl": "https://console.persql.com/approve/appr_…", "hits": [ { "tableGlob": "production_*", "action": "require_approval", "matchedTable": "production_users", "note": "…" } ], "expiresAt": "2026-05-05T12:30:00.000Z"}The SDK throws ApprovalRequiredError:
import { ApprovalRequiredError } from "@persql/sdk";
try { await db.query("UPDATE production_users SET …");} catch (e) { if (e instanceof ApprovalRequiredError) { console.log(`Open ${e.approvalUrl} to approve`);
// Block until a reviewer decides (or the token expires): const status = await db.approvals.poll(e.approvalToken, { intervalMs: 2000, timeoutMs: 10 * 60 * 1000, });
if (status.status === "approved") { const result = await db.approvals.redeem(e.approvalToken); } }}Or check status without blocking via db.approvals.get(token). For
long-running supervisors that want push-style notifications, register
a webhook on approval_required / approval_resolved (see below)
and let the platform call you instead.
deny rules throw PerSQLError(403) with no approval token — the
write can never run as long as the rule exists.
Reviewer flow
Section titled “Reviewer flow”The human opens console.persql.com/approve/{token}. They see:
- The full SQL (or batch).
- Every matched rule and the table it matched.
- Approve / Deny buttons.
Approve → the agent’s db.approvals.redeem(token) runs the original
SQL through the same auto-snapshot + query-log + pricing pipeline.
Deny → the agent’s redeem call returns 403.
The reviewer must be a member of the namespace.
Approvals expire after 30 minutes by default (max 24h). After expiry, the agent has to start over with a fresh request. The underlying KV row sweeps itself.
Webhooks
Section titled “Webhooks”Register a webhook (Console → Database → Webhooks) with the
approval_required and/or approval_resolved events. PerSQL POSTs
the payload to your URL when the corresponding event fires:
{ "id": "8f0…", "type": "approval.required", "ts": "2026-05-17T12:30:00.000Z", "database": "<do-id>", "approvalToken": "appr_…", "status": "pending", "hits": [{ "ruleId": "apprule_…", "tableGlob": "production_*", "action": "require_approval", "matchedTable": "production_users", "note": "…" }]}approval.resolved is identical except status is approved or
denied and type is approval.resolved. Use this to drive a Slack
notification, page an oncall reviewer, or kick a downstream workflow
that should run once the write actually executes.
Delivery uses the existing webhook queue — at-least-once with exponential backoff, signed with your webhook secret.
Strategic constraints
Section titled “Strategic constraints”- Glob-only matching. Row-shape predicates (
WHERE tier='paid') are not supported — use RLS for that. - Mutating SQL only. Reads pass through.
- Approvals are per-request, not per-table. An UPDATE touching three matched tables creates one pending approval, not three.