# Reaching production data without handing out the password

> A self-hosted portal that puts ad-hoc production SQL behind approval, masking and an immutable trail; it became the QueryProxy product.

- What it does today: Runs a developer's SQL against production through an approval step rather than directly; results are masked as they are written to disk and every request lands in an immutable record. Teams where production access sits with one person can run it today.
- Form: Open source, Web app
- State: Became a product
- Focus: Access governance & data protection
- Started: 2026-09-06
- Left Labs: 2026-09-08
- Technologies: PHP, Laravel, Livewire, Tailwind CSS, Vite, PostgreSQL, MySQL, Docker
- Tags: #sql, #access-control, #masking, #security
- Source code: https://github.com/QueryProxy/QueryProxy
- Website: https://queryproxy.com
- This one became a product: QueryProxy (https://www.muhammetsafak.com.tr/en/portfolio/queryproxy/)
- Source: https://www.muhammetsafak.com.tr/en/labs/queryproxy-lab/
- Language: en-US
- Author: Muhammet Şafak

---
If a bug shows up in production, that is also where it has to be diagnosed. But
handing the production password around is not a solution — and keeping it with
one person stops being one the moment that person takes a holiday.

## Looking at the data without holding the password

The attempt was this: let a developer read production data without ever seeing
the credentials. The connection details sit encrypted inside the application,
and the query runs on the portal rather than on the developer's machine. What
stays in the developer's hands is the SQL they wrote, and nothing else.

The cost is an accepted indirection: the query no longer runs the instant it is
written. In return the production password never reaches a human being, and one
person's calendar stops setting the team's diagnosis speed.

## The statement is parsed, not pattern-matched

A gate built on pattern matching would have been bypassed on day one — slip a
comment in, split the string, hide inside a subquery. Incoming SQL is parsed
instead: what a statement *is* comes from its resolved structure, not from its
text.

In practice an `UPDATE` or `DELETE` without a `WHERE` is refused, an unbounded
`SELECT` comes back with a default row limit, and anything touching database or
user management is rejected outright. Comments and string literals are
normalised, so writing `DROP/**/DATABASE` does not open the gate.

A limit expression the gate cannot understand is not clamped but **rejected** —
a bound you cannot read is a bound you cannot enforce. On top of that the
executor holds an absolute row ceiling that applies whatever the SQL claims, so
even a query that slips through cannot fill the result store.

## Approval runs through one service

Approvals arrive through three channels — the web UI, Slack, Teams — and all
three land in one service. Because the rule lives in a single place, adding a
channel does not duplicate it: a DBA approving their own request is closed
everywhere — a system admin can override it, and the override lands in the
audit record — and when two channels decide at once a conditional update drops
the second. In
chat callbacks the actor is resolved from an admin-managed mapping rather than
from an address the request declares about itself, so holding a shared signing
secret is not enough to approve as someone else.

## Masking happens on write, not on read

The last decision is where masking sits. Storing the result first and masking it
on display was the easy path; in that design unmasked data reaches disk once and
everything after it is a matter of hope.

Instead the result stream is read row by row and each row is masked **before it
is written to the file**. Not one unmasked cell enters the result store, and
both the viewer and the download read an already-masked file. The same
discipline applies to the rules themselves: a regular expression is stressed
against a long probe string when it is saved, and if it cannot be evaluated at
runtime the value is masked rather than left in the clear.

The Labs phase closed in three days — that is how clear the need was. QueryProxy
is a product with its own domain and releases now; the details are in the
portfolio.
