From npx to a comment on every pull request
Six short steps. The first needs nothing — no account, no config. The rest connect the hosted service: the run log and PR comments on Free, the full corpus and live-schema context on Startup and above.
1. Install and run the open-source CLI
The public bolvrk package is open source under the MIT license. It bundles 18 of the 69 documented SQL rules — the outage core and credential hygiene — plus the credential scanner, runs them locally, and never phones home. Exit codes: 0 clean, 1 findings, 2 usage or parse error, 3 service or database unavailable (nothing was checked — retry or fix the connection). --json emits the findings contract; --format=sarif emits one SARIF 2.1.0 log for code scanning.
Pass a multi-file set together — earlier files declare structure for later ones, and the BS rules only fire on a set.
npx bolvrk check migrations/*.sql # several files, globs, or - for stdin
npx bolvrk check migration.sql --json # structured findings for scripting
npx bolvrk secrets "src/**" ".env*" # credential rules over any file — always localbolvrk.json
A bolvrk.json at the repo root (found from any subdirectory) holds the defaults; bolvrk check with no files uses the migrations globs. The secrets globs are what bolvrk secrets scans with no arguments. --config=<path> points at another file and --no-policy ignores its policy block for one run. The policy block sets the block threshold only — per-rule overrides live on the dashboard's Policy page, never in a file a commit can change — and bolvrk policy pull writes the team's threshold into the file so local runs block where CI blocks. Team policy is a Scale feature.
{
"engine": "postgres",
"migrations": ["migrations/*.sql"],
"secrets": [".env*", "src/**"],
"policy": { "blockOn": "warning" }
}Suppressing a finding
A bolvrk-ignore comment on the statement's line or the line above suppresses a finding; several ids share one comment (BV003, BV034: reason). The reason is required, and every suppression — honored or not — is recorded in the report.
-- bolvrk-ignore BV003: index built in the maintenance window
CREATE INDEX idx_orders_region ON orders (region);
ALTER TABLE orders ADD COLUMN region text; -- bolvrk-ignore BV003, BV034: maintenance window Free in the CLI:
2. Connect the hosted service
The hosted service keeps the run log, comments on pull requests and sends notifications on every plan. On Free it runs the same free rules the CLI bundles; the rest of the corpus and tier-2 live-schema context (--db, stored connections) need Startup or above. It authenticates with a team token; the token is shown once, at creation.
Sign in with GitHub — the OAuth request carries no scopes beyond public identity.- Create a token: the first-run wizard at
/app/onboardingwalks you through it, or open Team → Tokens (/app/team/tokens) any time. It starts withblv_. - Add it to the repository as the
BOLVRK_TOKENsecret (Settings → Secrets and variables → Actions). - Invite teammates from Team → Members — invite links are valid for 7 days.
- Optional, Startup and above: add a stored connection under Connections and pass its id as
connection-idfor live-schema checks in CI.
3. Add the GitHub Action
Every pull request that touches a migration gets one evolving summary comment; the job fails on critical findings (or whatever your team policy blocks on). Analysis runs in the hosted service — the action is a thin client — so a Free team's checks run the free rules, and Startup and above run the full corpus; connection-id and live-schema refinement need Startup.
name: Migration check
on: pull_request
permissions:
contents: read # read changed files
pull-requests: write # post the summary comment
security-events: write # upload the SARIF log to code scanning
jobs:
bolvrk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required: the action diffs base...head to find changed migrations
- uses: bolvrk/bolvrk@v1
with:
token: ${{ secrets.BOLVRK_TOKEN }}
migrations: "migrations/*.sql" # default; any glob works
sarif-file: bolvrk.sarif # optional: findings for code scanning
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: bolvrk.sariffetch-depth: 0 is required — the action diffs base...head to find the changed migrations, and a shallow checkout has no base to diff against. Drop the upload-sarif step and security-events: write if you don't use code scanning.
Pull requests from forks have no secret and are skipped with a notice. A comment that cannot be posted is a warning, not a failure — the verdict follows the findings alone. The context sent with each check (the other migrations in the set) is capped at 20 MB; when the cap trims it, the action says so with a notice.
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
token | yes | — | Bolvrk team token (blv_...). Store it as a repository secret. |
migrations | no | migrations/*.sql | Glob for migration files, e.g. migrations/*.sql |
connection-id | no | — | Numeric id of a stored connection (dashboard → Connections). When set, checks run with live-schema (tier-2) context. |
api-url | no | https://bolvrk.com/api | Bolvrk API base URL. |
github-token | no | ${{ github.token }} | Token used to post the PR comment. Defaults to the workflow token. |
sarif-file | no | — | When set, write all findings as a SARIF 2.1.0 log to this path so a following github/codeql-action/upload-sarif step can publish them to code scanning. |
4. --remote and --db from the CLI
The same team token works from a terminal or any other CI. --remote sends the migration to the hosted service (BOLVRK_TOKEN authenticates; BOLVRK_API_URL overrides the endpoint, default https://bolvrk.com/api). On Free, --remote runs the free rules in the service — useful for the run log and PR comments; the full corpus needs Startup. --db gives the check live-schema context, and the service applies that snapshot on Startup and above. The CLI opens a READ ONLY transaction and only reads catalogs — point it at a replica or shadow database; we do not verify that it is one. Add --show-payload (requires --remote) to print the exact request body and exit without sending.
# through the hosted service — the run log records the check; the full corpus on Startup and above
BOLVRK_TOKEN=blv_... npx bolvrk check migrations/*.sql --remote# live-schema (tier-2) context from your own shadow database, structure only — Startup and above
npx bolvrk check migrations/*.sql --db=$SHADOW_DB
# both: introspect locally, push only structure — never rows, never a credential
BOLVRK_TOKEN=blv_... npx bolvrk check migrations/*.sql --db=$SHADOW_DB --remotebolvrk explain migration.sql --db=… plans the file's own statements with EXPLAIN inside a READ ONLY transaction that is rolled back — never executed — and reports sequential scans of large tables, nested loops and large sorts.
Never the primary: the security page documents the exact boundary — catalogs only, read-only transaction, nothing retained.
5. Upgrading
Plans are flat per team, never per seat. Open Billing in the app to move to Startup or Scale; the change applies to every token the team already has, and nothing on this page changes. Validation is never metered on any plan; AI reviews are 500 per month on Scale. Team policy — the block threshold and per-rule overrides — is a Scale feature.
6. Notifications
Under Notifications a team adds Slack or Discord incoming webhooks, or a generic HTTPS webhook. Each channel picks notifyOn: all runs, findings only, or critical only. Delivery is a single attempt with a 5 s timeout, https only, redirects not followed — a failed delivery never fails the check. The payload carries what the run log already stores: rule ids, severities, messages — never schema, never credentials.
Generic webhooks are signed. Every request carries X-Bolvrk-Event (check.completed), X-Bolvrk-Delivery (a unique id) and X-Bolvrk-Signature: sha256=<hex HMAC-SHA256 of the raw body with the channel secret>. The body is a versioned envelope (version: "1").
{
"event": "check.completed",
"version": "1",
"check": {
"id": 4211,
"migrationName": "0042_region.sql",
"source": "ci",
"findingCount": 1,
"worstSeverity": "warning",
"findings": [{ "ruleId": "BV003", "severity": "warning", "message": "…" }],
"createdAt": "2026-09-02T09:14:07.000Z"
}
}import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body as received, before any JSON parsing
function verify(rawBody, headers, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const given = headers['x-bolvrk-signature'] ?? '';
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}One command to start
Check a migration now, sign in when the team wants the rest.