Production, without the keys.
Visp DB Access is a self-hosted gateway for querying production PostgreSQL and MySQL. Every statement is parsed, checked against your access and the cluster’s policy, rewritten with hard limits, run in a guarded transaction, masked and audited. Engineers never see the database credentials.
docker pull ghcr.io/djkeshawa/visp-db-access:0.1.0
SELECT id, email, total FROM orders
WHERE created_at > now() - interval '1 day'
- recordSQL, identity, verdict and client IP saved before the database is touchedfirst
- limit… LIMIT 1001 · policy caps reads at 1,000 rows, +1 to detect truncationrewritten
- runread-only transaction · 15 s statement timeout · always rolled back37 ms
- routeread replica, healthy within the last two minutesreplica
- maskemail → ••••••masked
- UPDATEThe same engineer can’t write here: production policy blocks writes until you allow them, and then a second person approves each one.
email masked. Default production policy.- 1NetworkClient IP inside the org and cluster allowlists403
- 2AccessYour highest unexpired grant: read, write or admin403
- 3AnalyzeThe whole statement, including CTEs and subqueriesallow · approve · deny
- 4RecordSQL, identity and verdict saved before any database I/Ofails closed
- 5LimitRow cap, timeouts, cost gate, concurrency slot422 · 429
- 6ExecuteGuarded transaction; reads always roll backDatabase
- 7Mask, auditMasked columns hidden, final status in historyrecorded
Verdicts
Allow, approve, or refuse.
Choose a statement to see what the gateway decides and why. Writes hidden inside a read still count as writes.
SELECT * FROM orders WHERE email LIKE '%@example.com'
- Verdict
- allow read access is enough
- Runs as
- The same query with
LIMIT 1001appended, in a read-only transaction that is rolled back - Suggests
select_starandleading_wildcard: name the columns you need, and a pattern that starts with%can’t use an ordinary index. Suggestions never change the verdict.
WITH gone AS (DELETE FROM sessions WHERE expires_at < now() RETURNING id) SELECT count(*) FROM gone
- Verdict
- deny
writes_disabledon production - Why
- It looks like a
SELECT, but the CTE deletes rows. The gateway analyzes the whole statement, so this is a write, and production’s default policy blocks writes. - Recorded
- As
blockedin query history and the audit log
UPDATE users SET name = 'Maya Patel' WHERE id = 42
- Verdict
- needs approval risk high · staging · write access
- Next
- Request approval with a reason. A cluster admin who isn’t you approves or rejects it within 24 hours, and either of you runs it once.
- Guarded
- Both grants are checked again when it runs, and the write rolls back if it changes more than 1,000 rows
DELETE FROM orders
- Verdict
- deny
write_without_where - Why
- An
UPDATEorDELETEwithout aWHEREclause is refused in every environment, whatever your access. So is an always-true one such asWHERE 1 = 1.
SELECT pg_sleep(600)
- Verdict
- deny
dangerous_function - Why
- Sleeps, file access, advisory locks,
dblinkand similar functions are refused, along with sensitive catalogs such aspg_authidandmysql.user.
ALTER TABLE orders ADD COLUMN note text
- Verdict
- deny
ddl_disabledby default, in every environment - If allowed
- With
allow_ddlon the cluster’s policy, an admin can submit it, and it always goes through approval at riskcritical.
- Read-only
- The default. Writes need a
writegrant, a policy that allows them and, by default, a second person’s approval. DDL always needs approval. - Approvals
- Four eyes: a reviewer can never approve their own request. Approvals expire after 24 hours and are claimed once, so they can’t be replayed.
- Limits
- Statement and lock timeouts, row, byte and affected-row caps, an optional
EXPLAINcost gate, per-cluster concurrency limits, replica routing and cancellation on the server. - Credentials
- Encrypted with AES-256-GCM under your master key and write-only in the API. Engineers query through the gateway and never see them.
- Masking
- Column patterns such as
users.ssnor*.password*are masked in results, exports andRETURNING. - Discovery
- Finds RDS and Aurora databases across AWS accounts and regions with read-only permissions. Import is always an explicit admin action, and drift is flagged.
- One binary
- A Rust server with the React console embedded. Stateless nodes, all state in a PostgreSQL metadata store, a distroless Docker image and a Helm chart.
The gateway enforces
- Who may run what, on which cluster
- Hard limits on every statement
- Approvals for writes and DDL
- A record of every query and change
Still yours to set up
- Least-privileged database roles for the gateway
- Restricted views for truly sensitive data
- Outbound firewall rules to your databases
- Load-balancer affinity for cancellation
Get started
From a clone to a guarded query.
Docker Compose with sample PostgreSQL and MySQL databases. For production, use the image on GHCR or the Helm chart.
Deployment guide-
1
Choose the keys.
The server refuses to start without a valid 32-byte master key. Keep it in a secret manager: stored credentials can’t be decrypted with a different one.
git clone https://github.com/djkeshawa/visp-db-access && cd visp-db-access export VDA_MASTER_KEY="$(openssl rand -base64 32)" export VDA_BOOTSTRAP_ADMIN_PASSWORD='choose-a-long-random-password' -
2
Start it with demo data.
The
demoprofile adds PostgreSQL and MySQL databases with a sample shop.docker compose --profile demo up --build -
3
Register a cluster and query it.
Open
localhost:8080, sign in asadmin@example.com, and adddemo-postgres(databaseshop, userdemo, passworddemo-local-only) in thedevelopmentenvironment.
Keep going
Next in the lab · 05
Tierwise
Typed, layered configuration and decisions that explain themselves, with the same answer in Java and JavaScript.
Explore