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.

your rowsnever readsql valuesstripped before senddb accessoptional, catalogs, READ ONLYainoneself-hostone binary
01 · How it works

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.

02 · What crosses the wire

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. 1 · Your file

    Emails, keys, notes in comments. All still on your disk, where they stay.

  2. 2 · Client strips

    Every string literal becomes a placeholder, every comment is emptied. Then the request is built.

  3. 3 · The wire

    Stripped SQL, the schema's shape if you gave one, masked findings. Print it with --show-payload.

  4. 4 · Service strips again

    Same function, on receipt, so a client that forgot cannot leak.

  5. 5 · Stored

    Only the stripped form, with the findings. The snapshot is discarded.

Your file
-- 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);
What is sent and stored
--
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_stats only 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 SELECT on your tables, never a write.
  • String values or comments from the SQL. They are gone before the request is built.
  • Your connection string.--db opens a local connection and is never placed in the payload.
  • Secrets.bolvrk secrets refuses --remote outright. A credential scan never leaves the machine that ran it.
  • Query plans.bolvrk explain plans with EXPLAIN in a rolled-back read-only transaction, never ANALYZE, 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
03 · What we hold

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.
04 · Access

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.

ciphercrypto_box_sealsecret keyoutside the db

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.

at restsha-256revocationinstant

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.

access tokenmemory, 15 minrefreshhttpOnly cookie

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 scopesnoneappcontents read, PR write

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.

Reporting a vulnerability

Questions, disclosures, or anything that looks off: security@bolvrk.com. You'll get a human answer within two business days, credit if you want it, and no legal threats for good-faith research. If a report changes how we handle your data, this page changes with it. It is versioned alongside the code.