# Configuration

> Every QueryProxy environment variable — query limits, execution, result storage, trusted proxies, the application database and the queue.

- Documentation: Reference
- Updated: 2026-10-10
- Source: https://queryproxy.com/docs/configuration/
- Language: en-US
- Author: Muhammet Şafak

---
All configuration is environment variables (`.env` locally, container
environment in Docker). `.env.example` in the repository documents the full
set; this page explains the ones you'll actually change.

## QueryProxy settings

| Variable | Default | Meaning |
| :-- | :-- | :-- |
| `QUERYPROXY_SELECT_DEFAULT_LIMIT` | `1000` | `LIMIT` injected into SELECTs that have none |
| `QUERYPROXY_SELECT_HARD_LIMIT` | `10000` | Larger LIMITs are clamped down to this |
| `QUERYPROXY_EXECUTION_TIMEOUT` | `300` | Max seconds a single request may run on the worker |
| `QUERYPROXY_RESULT_DISK` | `local` | Laravel filesystem disk for result files (`s3` supported); a publicly visible disk is refused |
| `QUERYPROXY_RESULT_TTL_DAYS` | `30` | Days before `results:prune` deletes stored results |
| `QUERYPROXY_SEED_DEMO` | `false` | Docker only: seed the demo team on first boot (one random password, printed once in the logs; refused under `APP_ENV=production` unless `QUERYPROXY_SEED_DEMO_FORCE=true`) |
| `QUERYPROXY_ADMIN_EMAIL` / `QUERYPROXY_ADMIN_PASSWORD` | — | Docker only: create the first system administrator on boot while the instance has no users (password at least 8 characters); a no-op once any user exists |
| `QUERYPROXY_ADMIN_NAME` | `Admin` | Docker only: display name for that first administrator |
| `QUERYPROXY_RUN_WORKER` | `true` | Docker only: run the queue worker inside the container; set `false` when the worker runs in its own container |
| `QUERYPROXY_RUN_SCHEDULER` | `true` | Docker only: run the scheduler (`schedule:work`) inside the container |
| `QUERYPROXY_WORKER_PROCESSES` | `1` | Docker only: number of queue worker processes |
| `QUERYPROXY_KEY_FILE` | `/var/www/html/storage/app/.app_key` | Docker only: where the generated `APP_KEY` is persisted when `APP_KEY` itself is not set. When the variable is unset and a key file already exists at the legacy path `/var/www/html/database/.app_key`, the entrypoint keeps using that file |
| `QUERYPROXY_REQUIRE_2FA` | `none` | Enforce TOTP two-factor auth: `none`, `admins`, `dba` (admins plus anyone with a DBA role) or `all` |
| `QUERYPROXY_CHAT_WEBHOOK_ALLOWED_HOSTS` | — | Extra allowed hosts for outbound chat webhooks, comma-separated, `*` wildcards allowed (SSRF guard; `hooks.slack.com`, `*.webhook.office.com` and `*.logic.azure.com` are allowed by default) |
| `QUERYPROXY_CHAT_INCLUDE_SQL` | `true` | Embed a SQL preview in chat notifications |
| `QUERYPROXY_CONNECTION_HOST_DENYLIST` | — | Extra hosts blocked as connection targets (comma-separated) |
| `QUERYPROXY_SQLITE_ALLOWED_DIR` | — | Restrict SQLite connection files to this directory. Files inside the application directory (where QueryProxy's default SQLite database lives) are always rejected, whether or not this is set |
| `QUERYPROXY_EXTRA_UNTRUSTED_LANGUAGES` | — | Extra procedural languages the guard refuses in `CREATE EXTENSION` (comma-separated); extends the built-in list. `DO`, `CREATE FUNCTION` and `CREATE PROCEDURE` accept only `plpgsql` and `sql` regardless |
| `QUERYPROXY_EXTRA_DANGEROUS_VARIABLES` | — | Extra server variables the guard refuses in `SET` (persistent scopes on MySQL / MariaDB, every scope on PostgreSQL) and in `set_config` (comma-separated); extends the built-in list |
| `QUERYPROXY_EXTRA_BLOCKED_FUNCTIONS` | — | Extra functions the guard refuses outright (comma-separated); extends the built-in list |
| `QUERYPROXY_EXTRA_STATE_CHANGING_FUNCTIONS` | — | Extra functions that make a statement a write (comma-separated); extends the built-in list |
| `QUERYPROXY_EXTRA_RESOURCE_CONSUMING_FUNCTIONS` | — | Extra functions that make a statement a write because they hold resources, such as sleeps (comma-separated); extends the built-in list |
| `QUERYPROXY_EXTRA_BLOCKED_PROCEDURES` | — | Extra SQL Server procedures the guard refuses (comma-separated); extends the built-in list |
| `QUERYPROXY_EXTRA_SQLITE_PRAGMAS` | — | Extra SQLite pragmas allowed in their bare, read form (comma-separated); extends the built-in list |
| `TRUSTED_PROXIES` | — | Reverse proxy IPs/CIDRs allowed to set `X-Forwarded-*` (comma-separated); empty trusts no proxy |
| `TRUSTED_HOSTS` | — | Extra hostnames this installation answers to, beyond `APP_URL`'s host (comma-separated) |
| `DB_QUEUE_RETRY_AFTER` | `QUERYPROXY_EXECUTION_TIMEOUT` + 30 (`330`) | Seconds before a reserved queue job is retried. Derived from the execution timeout when unset; an explicit value wins. `BEANSTALKD_QUEUE_RETRY_AFTER` and `REDIS_QUEUE_RETRY_AFTER` work the same way |

> `retry_after` is derived: unless you set `DB_QUEUE_RETRY_AFTER` (or the
> Beanstalkd / Redis equivalent) yourself, it is `QUERYPROXY_EXECUTION_TIMEOUT`
> + 30 seconds (`330` for the default `300`). The Docker entrypoint derives the
> worker `--timeout` (execution timeout + 10) and `stopwaitsecs` (execution
> timeout + 20) from the same setting. If you set `retry_after` explicitly, keep
> it above the execution timeout, because the query job is stopped at that
> point; the default +30 leaves a margin. A value at or below it lets a slow
> query be re-reserved while it is still running and then marked failed, even
> though its write may commit, and logs a warning when `queue:work` (daemon or
> `--once`) or `queue:listen` starts. A fixed `*_QUEUE_RETRY_AFTER=330` left
> over from the 0.2.0 `.env.example` stops the derivation, so remove it unless
> you set it on purpose.

A TOTP code accepted for `QUERYPROXY_REQUIRE_2FA` is **burned**, so it cannot be
replayed for the rest of its window. One practical consequence: signing in on a
second device within the same 30-second step means waiting for the next code.

`QUERYPROXY_EXTRA_UNTRUSTED_LANGUAGES` and `QUERYPROXY_EXTRA_DANGEROUS_VARIABLES`
add to the two lists the [SQL guards](/docs/sql-guards/) judge `CREATE EXTENSION`
and `SET` by. They can only **extend**: the built-in entries are
the floor, merged back on top of whatever the environment provides, so a trimmed
list cannot disarm the guard.

### MySQL / MariaDB targets: `sql_mode`

The guard reads quotes and backslashes the default way, so a request is refused
when the session `sql_mode` would read them differently. Before it runs the SQL
on a `mysql` or `mariadb` connection, QueryProxy reads `@@SESSION.sql_mode`; if
it contains `NO_BACKSLASH_ESCAPES` or `ANSI_QUOTES`, the request fails and
nothing runs, with a message naming the flag. QueryProxy itself sets a strict
`sql_mode` when it connects, which replaces the server's default, so if this
fires, look for what changes the mode after connect (a proxy in between, a
changed connection configuration); keeping the flags out of the server and
connection-user defaults is still sensible, but on its own it will not clear
it. Submitting a `SET` that changes `sql_mode` is refused on every
driver (see [SQL guards](/docs/sql-guards/)).

## Application key and HTTPS

`APP_KEY` encrypts your stored connection credentials and chat secrets. **For
production, set it explicitly and keep it stable** — a regenerated key (e.g. on
container recreation) makes every stored credential unreadable. Generate one
with `php artisan key:generate --show`, and when rotating, move the old key into
`APP_PREVIOUS_KEYS` (comma-separated) so existing ciphertext still decrypts.

When serving over HTTPS behind a reverse proxy, set `SESSION_SECURE_COOKIE=true`.

## Behind a reverse proxy

> **Breaking change — set `TRUSTED_PROXIES` when a proxy sits in front of
> QueryProxy.** Earlier versions trusted every proxy. From this version no proxy
> is trusted by default, and until you name yours the app reads the connecting
> peer as the client.

`TRUSTED_PROXIES` takes a comma-separated list of the IPs or CIDRs allowed to
set `X-Forwarded-*` headers:

```dotenv
TRUSTED_PROXIES=10.0.0.0/8
```

Until that list names your proxy, `X-Forwarded-Proto` is ignored — HTTPS is not
detected, so no HSTS header is sent — and audit entries record the proxy's
address instead of the client's.

**Never set it to `*`.** Trusting every proxy lets any client send its own
`X-Forwarded-For`, which forges the client IP in the audit trail and hands out
unlimited login and 2FA attempts, because both throttles are keyed per IP.

`X-Forwarded-Host` is not honoured at all, whatever the list says: a forged host
header rewrote the link in password-reset mail. As a second layer, the request
host is pinned to `APP_URL`'s host (and its `www.` sibling), so absolute URLs —
signed routes, password-reset mails — always point at your own installation. Set
`APP_URL` to the address users actually reach.

If the installation answers to more than one name, list the others in
`TRUSTED_HOSTS`:

```dotenv
TRUSTED_HOSTS=queryproxy.internal,db.example.com
```

That is the supported way to run a multi-domain deployment now that
`X-Forwarded-Host` is ignored — a hostname you have not listed still cannot
rewrite a password-reset link. The names are added to `APP_URL`'s host (and its
`www.` sibling), never replace it. Entries that do not parse as a hostname are
dropped silently, so a typo cannot take the installation down.

Both values are read through the application config, so setting them in `.env`
is enough. If you cache the configuration, re-run `php artisan config:cache`
after changing them; the Docker image re-caches on every start.

## Application database

QueryProxy's own data (users, teams, requests, audit log) lives in SQLite by
default — zero configuration. For MySQL or PostgreSQL, set the standard Laravel
variables:

```dotenv
DB_CONNECTION=pgsql        # or mysql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=queryproxy
DB_USERNAME=queryproxy
DB_PASSWORD=change-me
```

This is the *application* database — the databases you proxy are configured in
the UI under **Connections**, per team, with encrypted credentials.

## Queue

The queue is database-backed by default (no Redis required). To move it to
Redis:

```dotenv
QUEUE_CONNECTION=redis
REDIS_HOST=redis
```

`REDIS_QUEUE_RETRY_AFTER` is optional: when unset it is derived from
`QUERYPROXY_EXECUTION_TIMEOUT` + 30 seconds (see the note under the settings
table). Only set it to override that value, and keep it above the execution
timeout.

The worker command stays the same. Keep its `--timeout` a little above the
execution timeout (execution timeout + 10 seconds; `QUERYPROXY_EXECUTION_TIMEOUT`
is `300` by default, hence `310`):

```bash
php artisan queue:work --queue=queries,default --tries=1 --timeout=310
```

In a manual setup this number is not derived for you: if you change
`QUERYPROXY_EXECUTION_TIMEOUT`, change `--timeout` to match (the Docker
entrypoint derives it itself).

The same warning (`retry_after` at or below the execution timeout) is logged
once when `queue:listen` or `queue:work --once` starts, not only for a daemon
`queue:work`. The short-lived `queue:work --once` processes that `queue:listen`
spawns do not repeat it.

In the repository's `docker-compose.yml`, the separate `worker` example no
longer hard-codes `--timeout=310`: it passes `QUERYPROXY_WORKER_TIMEOUT`, which
the image entrypoint derives as `QUERYPROXY_EXECUTION_TIMEOUT` + 10. Set
`QUERYPROXY_EXECUTION_TIMEOUT` on that service and the worker follows. This
needs the image's default entrypoint, so do not override `entrypoint:` on the
worker service.

## Result storage on S3

Set `QUERYPROXY_RESULT_DISK=s3` and the standard `AWS_*` variables. Result
files are written as NDJSON, streamed — memory use stays flat regardless of
result size.

The disk must be **private**. Result paths are predictable
(`results/{team_id}/{id}.ndjson`), so a publicly served disk would hand out
every team's rows straight from the web root, bypassing the download
authorization entirely. QueryProxy therefore refuses to write results when
`QUERYPROXY_RESULT_DISK` points at the `public` disk or at any disk configured
with `visibility => public`, and the request fails with an explicit message
instead.

## SQL Server targets

The default Docker image ships `pdo_pgsql`, `pdo_mysql` and `pdo_sqlite`. To
proxy Microsoft SQL Server databases, extend the Dockerfile with Microsoft's
ODBC driver and the `sqlsrv` / `pdo_sqlsrv` PECL extensions, then build and run
your own image in place of `queryproxy/queryproxy`. The extensions have to be
present in the PHP build that runs both the web process and the queue worker,
because approved queries execute on the worker.
