Security
A verification service for your database changes deserves a plain answer about what we store, what we never see, and what a breach of us would expose of you — especially when the changes we verify are increasingly written by AI.
What we store — and what we never do
Stored
- Migration SQL and findings — the text you sent for checking and the full findings report it produced (rule ids, severities, messages, object names, suppressions). This IS the product's run log: every connected check, kept so your team can see what went wrong and how often.
- Git metadata — repository, branch, pull-request number and commit for checks the Action or CLI sends, so a check links back to where it came from.
- AI review observations — when you explicitly request a review, the model's advisory commentary is stored alongside the check it reviewed.
- Rule IDs and object names in findings history — e.g.
BV003fired onorders. - Connection strings, sealed — encrypted with a libsodium sealed box before insert. The keypair lives outside the database, so a database breach alone yields ciphertext.
- Credential hashes — SHA-256 of team tokens and refresh tokens, never the plaintext.
- Your GitHub id and login — the whole identity we request at sign-in.
Never stored, never read
- Rows from your tables — introspection runs in a
READ ONLYtransaction and queries onlypg_catalogandinformation_schema; the onboarding role grants catalog access and nothing else. - Full schema dumps — findings reference rule IDs and object names, not your schema.
- Plaintext credentials — token values render exactly once, at creation, in your browser.
- GitHub repo access — the OAuth request carries no scopes beyond public identity; we could not read your code if we wanted to.
The life of a tier-2 check
Most rules refine their verdict against a live snapshot — escalating on large or hot tables, going silent on provably empty ones; only a few need one to fire at all. Live-schema context exists for exactly as long as one check runs. Nothing from your database survives the request. In credential-free mode the first two steps happen on your machine — the CLI introspects locally and only the structure crosses the wire — and the lifecycle below is the stored-connection variant.
- Unseal, connect
Your connection string is opened from its sealed box just-in-time — for this check only, never in bulk — and a short-lived connection is made to the database you stored.
- Introspect catalogs, read-only
Inside a
READ ONLYtransaction, we read table sizes, columns, foreign keys, and indexes from the system catalogs. No query touches your tables. - Check in memory
The rule corpus runs over the migration's parse tree with the schema snapshot as context. The snapshot lives in process memory for the duration of the check.
- Discard
The connection closes and the snapshot is gone. What persists is the findings report: rule IDs, severities, object names.
How we keep the surface small
Never the primary
The CLI and the service open a READ ONLY transaction and only read catalogs. Point them at a replica or shadow database — we do not verify that it is one. The onboarding SQL we hand you grants catalog access and nothing else.
AI is opt-in, and never the verdict
No SQL or schema data reaches a model unless you explicitly request an AI review of a specific migration — and what comes back is clearly-labeled advisory commentary. It cannot pass or fail a check, and Bolvrk never generates SQL. The deterministic engine alone decides; that is the whole point of a verifier.
Every rule is documented
The CLI is open source (MIT) and bundles the free rule set; the corpus beyond the free set is proprietary and runs only in the hosted service. What we can show you is exactly what each rule does: every rule has its own page with the rationale, the SQL it fires on and the safe fix — the same fixtures our CI asserts. The CLI talks to your database directly; nothing touches our servers unless you pass --remote.
Boring on purpose
One service, one Postgres, no microservices, no message brokers, no third-party analytics in the product. A small surface is the cheapest security control there is.
The strongest option: never give us a credential at all
The CLI and CI runner already sit next to your database — so they do the introspection, and send us only the result: schema structure. Table and column names, row estimates, index definitions, the foreign-key graph, write-rate counters. Never rows, never values, never a connection string. The check runs on our side against that metadata, lands in your team's history, and the snapshot itself is discarded — malformed snapshots are rejected outright rather than repaired into a weaker check.
BOLVRK_TOKEN=blv_... bolvrk check migration.sql --db=$SHADOW_DB --remote
Don't take our word for it: bolvrk check migration.sql --db=$SHADOW_DB --remote --show-payload prints the exact request body and exits without sending — audit every byte that would leave your machine.
bolvrk explain follows the same rule for query plans: the file's own statements are planned with EXPLAIN inside a read-only transaction that is rolled back — never EXPLAIN ANALYZE, never executed — and only the plan tree leaves the database.
Storing a connection with us (sealed-box encrypted, opened per-check) remains available for teams that prefer zero-setup CI — but it is optional, not the price of tier-2.
The credential rules apply the same discipline to your own secrets. When a migration carries a plaintext password, a connection string or an API key, the finding masks the value before it is written anywhere — the PR comment, the run log and the SARIF file never repeat it. And bolvrk secrets, the same rules over any file, has no remote mode at all: a credential scan never leaves the machine that ran it.
Outbound notifications carry only what the run log stores — rule ids, severities, messages — and generic webhooks are HMAC-signed (X-Bolvrk-Signature); see the docs.
Credentials, in detail
Team tokens
Authenticate the CLI and CI. Stored as SHA-256 hashes and compared in constant time; the plaintext is shown once at creation. Revocation is immediate and per-token, so a leaked CI secret is one click from dead.
Dashboard sessions
GitHub OAuth with a CSRF-bound state. The browser holds a 15-minute access token in memory only — never in storage — and a long-lived refresh token in an httpOnly cookie scripts cannot read. Refresh tokens are stored hashed and revoked server-side on logout.
Connection strings
Sealed-box encrypted (libsodium): sealing needs only the public key, opening needs the secret key held outside the database in the secret manager. Decryption happens per-check, never in bulk, and never lands in logs.
What GitHub gives us
The OAuth request has an empty scope list: public id and login, nothing else. No repo contents, no org membership, no email scope.