Zero-data control plane
Production-realism without production risk
Your data never touches our servers. The Subveil CLI runs in your environment, on your laptop or your CI runner. Database rows, PII values, credentials and the masking seed never go to Subveil; Cloud is a zero-data policy and compliance control plane for metadata only.
The cloud stores account and control-plane metadata, never database row values.
Two boundaries · one local data path
Data plane and control plane stay separate
The CLI reads and masks rows where you run it. Cloud sends policies to the CLI and receives schema and execution metadata over HTTPS.
Data plane · your network
Laptop · VPC · GitHub Actions runnerSource DB
Postgres · MySQL · MariaDB
Subveil CLI
FK graph · HMAC masking before load
Target DB
Local · ephemeral PR DB
Raw rows, PII values, database credentials and SUBVEIL_SEED stay in this boundary.
HTTPS
SUBVEIL_API_KEY
policy down · metadata up
Subveil Cloud · control plane
Published masking policy
Policy document + policy_hash
Schema catalog
Table and column names + PII coverage status
Run and branch metadata
Signed audit receipts + PR lifecycle status
From schema to safe test data
The four-step workflow
Review the generated policy, choose the slice you need, and run it locally or in CI. Cloud features are opt-in and work on metadata only.
- 01 / 04
Discover likely PII and schema drift
subveil initinspects column names and catalog types, then samples up to 50 values from eligible text and JSON columns by default. Type and name rules help classify fields; sampling can spot patterns their names miss. The sample runs on the CLI machine: values aren't printed, written tosubveil.yaml, or sent to Cloud. Treat detections as suggestions and review the generated file.local discovery + schema report $ subveil login$ subveil init --project prj_abc123def456 --report-drift --sample-rows 50example generated masking suggestions tables: public.customers: columns: email: { transformer: email } full_name: { transformer: name } phone: { transformer: phone } ssn: { transformer: token }Column names
Suggest email, name, phone and identifier fields.
SQL types
Help choose compatible rules and rule out impossible values.
Local sampling
Look for PII patterns in up to 50 eligible values per column.
When the schema is reported, Cloud checks catalog names and types against the published policy. It can flag likely unmasked PII such as
public.customers.ssn; this is a drift signal, not a guarantee that every sensitive field was found. - 02 / 04
Subset the graph and mask before loading
Root filters select starting rows. Subveil follows the foreign-key graph to include referenced and referencing rows needed by the slice, then masks each row before it is written to the target. The same
SUBVEIL_SEEDkeeps deterministic mappings stable across runs; unique columns get collision-safe variants when needed.subveil.yaml · masking + subset excerpt version: 1engine: postgresmasking: seed_env: SUBVEIL_SEEDsubset: roots: - table: public.accounts scale: 0.05 default_scale: 0.05 - 03 / 04
Test migrations on every pull request
The generated
.github/workflows/subveil-pr.ymlstarts a service database on the runner (PostgreSQL 18 by default, or MySQL 8.4), loads a masked copy, then runs your migrations and tests against it. Constraint failures can surface before merge. Subveil reports the PR database lifecycle to the dashboard; only status metadata is sent.Representative steps · configure secrets in GitHub.github/workflows/subveil-pr.yml # .github/workflows/subveil-pr.yml (representative steps)jobs: database: runs-on: ubuntu-latest services: postgres: image: postgres:18 env: { POSTGRES_PASSWORD: subveil } ports: ["5432:5432"] env: SUBVEIL_API_KEY: ${{ secrets.SUBVEIL_API_KEY }} SUBVEIL_SOURCE_DSN: ${{ secrets.SUBVEIL_SOURCE_DSN }} SUBVEIL_SEED: ${{ secrets.SUBVEIL_SEED }} SUBVEIL_PROJECT_ID: prj_abc123def456 SUBVEIL_TARGET_DSN: postgres://postgres:subveil@localhost:5432/postgres?sslmode=disable steps: - run: subveil pull --config subveil.yaml --project "$SUBVEIL_PROJECT_ID" --create-schema --require-audit - run: npx prisma migrate deploy env: { DATABASE_URL: ${{ env.SUBVEIL_TARGET_DSN }} } - run: npm test env: { DATABASE_URL: ${{ env.SUBVEIL_TARGET_DSN }} } - if: always() run: subveil ci report --status "${{ job.status }}"Example migration and test commands are for a Prisma/Node project; replace them with your project's migration and test commands. Fork pull requests are skipped because GitHub does not provide repository secrets to them. The full workflow is generated on the Branches tab of your dashboard.
On a GitHub-hosted runner, source rows pass through that runner while the CLI copies them. If rows must stay inside your own network, use a self-hosted runner.
Before using
--require-audit, publish a project policy; the CLI stops before loading the target if the required policy cannot be fetched. - 04 / 04
Keep a verifiable run receipt
A cloud-mode pull submits a receipt with a SHA-256 hash of its canonical contents and an HMAC signature tied to the API token. The append-only ledger verifies the receipt hash and shows the policy hash, hashed source and target identifiers, row counts, and masked column names.
Receipts give your team verifiable records to support SOC 2, ISO 27001, HIPAA, and GDPR evidence gathering. They do not certify that an entire environment is free of raw PII or guarantee auditor acceptance.
The displayed sample is illustrative; real receipts contain your run metadata and hashes, never row values.verified receipt · shortened example { "run_id": "9d1c…e2f4", "status": "success", "source_host_hash": "4be1…9c07", "policy_hash": "9f2c…b71e", "total_rows_copied": 50000, "tables_summary": [ { "table": "public.customers", "rows_copied": 50000, "masked_columns": ["email", "full_name", "ssn"] } ], "receipt_sha256": "8a72…f310", "signature": "c41e…07ab"}
Technical guarantees
Built for real database shapes
Compatibility floors and the versions exercised in CI are shown separately, so you can see what is tested and what is only expected to work.
| Engine | Supported floor | Exercised in CI |
|---|---|---|
| PostgreSQL | 14+ | 16 |
| MySQL | 8.0+ | 8.0 · 8.4 |
| MariaDB | 10.6+ | 10.6 |
Versions above the floor are expected to work but are not in the CI matrix. Managed-provider configurations (RDS, Cloud SQL and similar) are not covered either.
Built-in transformers
Mask common sensitive fields
Transformer IDs match the CLI. custom_sql runs only from your local config; Cloud policies do not accept it. Quote nullin YAML so it is not parsed as an empty value.
- name
- phone
- address
- uuid
- token
- ip
- number
- date_shift
- redact
- "null"
- custom_sql
- jsonb
Security FAQ
Questions security teams ask
Do you store database connection strings or credentials?
No. Source and target DSNs, database passwords, and SUBVEIL_SEED stay in your environment or CI secret store. Subveil Cloud receives an API token over HTTPS for its own authentication, but it does not receive your database credentials or masking seed.
How does Subveil preserve foreign-key integrity?
You choose root rows with a filter or sample scale. The CLI follows the foreign-key graph to include the related rows the slice needs, plans a valid load order, and handles cycles and self-references with engine-specific strategies. Run subveil plan to inspect the graph before loading.
What happens when a migration adds a PII column?
When you report a schema, the CLI sends catalog metadata such as table and column names and types, not values. The dashboard compares likely PII columns with the published project policy and can flag candidates with no rule. This is a useful drift signal, not proof that every possible PII field has been found.
Can we run the CLI offline?
Yes. Local init, plan, and pull can use a local subveil.yaml without contacting Subveil Cloud. Your source and target databases still need to be reachable from the machine running the CLI. Central policy sync, schema reporting, branch status, and cloud audit receipts need network access to Subveil Cloud.
Start with a local copy
Keep production rows where they belong.
Install the free CLI, review the generated policy, and build a masked database in your own environment.