Security
Bolvrk reads migration SQL and checks it against a rule set. It does not need your data to do that, so it never gets it. This page says what runs where, what leaves your machine, and what a breach of us would expose of you. Every claim on it is checked against the code.
A check is a parse, a rule pass, and a report
The migration is parsed with the real Postgres parser into a syntax tree. Each rule walks that tree and, when you give it one, a snapshot of your schema's shape: table and column names, types, row estimates, indexes and their usage counters, foreign keys. The output is a list of findings: rule id, severity, a message, the object it points at. No step needs a row of your data, and no step involves a model.
On your machine
bolvrk check runs the whole thing locally. The only network call the CLI can make is to the service, and it makes it only when you pass --remote. No telemetry, no update check, no install script. Add --db and it reads your schema's shape itself, inside a READ ONLY transaction with a statement timeout.
Through the service
With --remote, the GitHub Action, or the GitHub App, the check runs on our side and lands in your team's run log. What is sent is shown below, and you can print it before anything is sent. Storing a connection with us is optional. It exists so CI can see your live schema when the runner cannot reach your database.
On your own servers
bolvrk-server is the whole service as one binary: the API, the dashboard, the full rule set, one config file, one Postgres. Nothing it does reaches bolvrk.com. It is the answer for teams whose migrations may not leave the building.
Values are stripped before the request exists
Rules are about structure, so structure is all we take. Before the CLI, the Action or the App builds a request, every string literal and every comment in the migration is replaced. Identifiers and numbers stay, because the rules are about them. The service strips again on receipt and stores only the stripped form, so the guarantee does not depend on which client sent the check.
- 1 · Your file
Emails, keys, notes in comments. All still on your disk, where they stay.
- 2 · Client strips
Every string literal becomes a placeholder, every comment is emptied. Then the request is built.
- 3 · The wire
Stripped SQL, the schema's shape if you gave one, masked findings. Print it with
--show-payload. - 4 · Service strips again
Same function, on receipt, so a client that forgot cannot leak.
- 5 · Stored
Only the stripped form, with the findings. The snapshot is discarded.
-- backfill for ticket 4821, ask anna@example.com
UPDATE users
SET plan = 'pro',
api_key = 'sk_live_51H8zK2'
WHERE email = 'anna@example.com'
AND signup_year > 2021;
CREATE INDEX users_plan_idx ON users (plan);--
UPDATE users
SET plan = 'bolvrk.redacted.value',
api_key = 'bolvrk.redacted.value'
WHERE email = 'bolvrk.redacted.value'
AND signup_year > 2021;
CREATE INDEX users_plan_idx ON users (plan); The one exception is SET and RESET statements, whose literals are settings such as statement_timeout = '0', which the rules must see. A test in each client builds a real request from SQL containing an email, a phone number and a key, and asserts none of them is in the bytes sent.
Sent
- The stripped migration, plus neighbouring migrations for context, stripped the same way.
- The schema's shape, when you provide it: table, column and index names, types, row-count estimates, index usage counters, foreign keys, per-table write counters, server version, and the connecting role's name. From
pg_statsonly distinct-count and null fraction, never common values. - Repository ignores from
bolvrk.json, and from CI the repo, branch, PR number and commit. - Masked credential findings, when the local credential rules fired: the rule, the line, the value replaced by
REDACTED.
Never sent
- Rows from your tables. Introspection reads system catalogs only, nine fixed statements pinned by a test. Never a
SELECTon your tables, never a write. - String values or comments from the SQL. They are gone before the request is built.
- Your connection string.
--dbopens a local connection and is never placed in the payload. - Secrets.
bolvrk secretsrefuses--remoteoutright. A credential scan never leaves the machine that ran it. - Query plans.
bolvrk explainplans withEXPLAINin a rolled-back read-only transaction, neverANALYZE, and the plan stays local.
Don't take our word for it. --show-payload prints the exact request body a --remote check would send and exits without sending it. Put the output in your security review.
bolvrk check migration.sql --db=$SHADOW_DB --remote --show-payload
What a breach of us would expose
The service is one Bun process and one Postgres. No queues, no workers, no message brokers, no error tracker, no analytics inside the product. Here is everything the database holds about you, and the parts that would still be useless to an attacker.
Stored
- The run log: stripped migration SQL, the findings report, git metadata, and the object names the findings point at. This is the product. Retention is yours to set, from 7 days to forever, and the whole log exports as CSV or JSON.
- Connection strings, sealed. Encrypted with a libsodium sealed box before insert. The secret key is an environment secret of the running API and is not in the database, so a database breach alone yields ciphertext.
- Token hashes. SHA-256 of team tokens and refresh tokens. The plaintext is shown once, at creation, and never again.
- Your GitHub id and login. The whole identity we ask for at sign-in. No email, no avatar.
Not there
- Schema snapshots. They live in process memory for one check and are discarded. There is no column for them. Malformed snapshots are rejected, not repaired.
- Values from your SQL. Stripped on the client and again on receipt.
- Plaintext credentials. Connection strings are never logged and are shown only with the password masked.
- Model prompts or outputs. Bolvrk runs no AI. No SQL or schema data reaches a model through us, and no model decides a verdict.
Credentials and what they can reach
A stored connection
Opened from its sealed box only when needed: for a check, for the 15-minute reachability probe, for a connection test you click, and on Team plans for the scheduled schema health audit. Each time it is a short-lived connection, a READ ONLY transaction with a statement timeout, catalog queries only, then disconnect. Point it at a replica or shadow database with the catalog-only role from the setup guide. We do not verify that it is one.
Team tokens
Authenticate the CLI and CI. Looked up by their SHA-256 hash, never stored in plaintext. Every token records where it was last used. Its first use and every new address are audit events, so a leaked CI secret used from somewhere unexpected leaves a trace. Revocation is per token and immediate.
Dashboard sessions
GitHub sign-in with a CSRF-bound state. The browser holds a 15-minute access token in memory and a refresh token in an httpOnly cookie scripts cannot read. Refresh tokens are stored hashed and revoked server-side on logout.
GitHub
Sign-in requests no scopes: public id and login, nothing else. The optional GitHub App is different and we say so: it needs read access to repository contents to fetch the migration files in a pull request, plus write access to comments and checks to report back. What it reads is stripped and checked like any other migration. It never connects to your database.
Sign-in and refresh are rate limited per address, checks per team, and every response refuses framing and content sniffing. Outbound notifications carry rule ids, severities and messages, never SQL. Generic webhooks are HMAC-signed (X-Bolvrk-Signature), HTTPS only, and destinations are re-resolved at send time with private addresses refused. The CLI and its free rules are Apache-2.0; the engine core is FSL-1.1. Every rule has a page with the SQL it fires on and the safe fix, the same fixtures our CI asserts.