QueryProxy
Documentation menu

Configuration

Every QueryProxy environment variable — limits, timeouts, TLS, secrets, SCIM, Slack, schema explorer, metrics, SIEM export, proxies, database, queue.

Updated:

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. A whole number of seconds greater than zero; anything else (text, empty, 0, negative, decimal, a value with a unit) falls back to 300. The upper limit is 86400 seconds (24 hours): a valid value above it is not an error, it is clamped to 86400. The Docker entrypoint prints an [entrypoint] warning when it clamps the value. The database-side statement timeout is derived from it (the execution timeout minus 5 seconds, at least 1), so the database stops before the job does; see Read credentials and timeouts
QUERYPROXY_LOCK_TIMEOUT 5 Seconds a statement waits for a lock on the target database before the database gives up. Digits only, 1 to 3600. A larger value is clamped to 3600; anything else (text, empty, 0) falls back to 5
QUERYPROXY_CONNECTION_SLOTS 2 How many requests may run on one target connection at the same time. The rest stay queued and start when a slot frees up; 0 switches the limit off
QUERYPROXY_CONNECTION_SLOT_RETRY_DELAY 5 Seconds before a request that found every slot busy tries again
QUERYPROXY_SCHEMA_TIMEOUT 5 Statement timeout, in seconds, of the schema explorer’s catalog read. See Schema explorer
QUERYPROXY_SCHEMA_CACHE_TTL 3600 Seconds a connection’s schema stays cached; 0 turns the cache off
QUERYPROXY_SCHEMA_REFRESH_LIMIT 3 Manual schema refreshes a user may ask for per connection and minute
QUERYPROXY_SCHEMA_MAX_COLUMNS 20000 Most columns one schema read returns; a larger schema is cut at a table boundary
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. The Helm chart sets it per Deployment (see Kubernetes)
QUERYPROXY_RUN_SCHEDULER true Docker only: run the scheduler (schedule:work) inside the container; keep exactly one scheduler per installation
QUERYPROXY_WORKER_PROCESSES 1 Docker only: number of queue worker processes
QUERYPROXY_WORKER_TIMEOUT execution timeout + 10 Worker --timeout, derived by the Docker entrypoint from QUERYPROXY_EXECUTION_TIMEOUT (the same applies in a separate worker container). It cannot be overridden: the entrypoint recomputes it on every start and ignores a value you pass. Change the execution timeout instead. Outside Docker, set --timeout on queue:work yourself
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_LOCAL_LOGIN all Scope of password login: all, admins_only (only system administrators use a password, for break-glass access) or none (password login and password reset are off, so SSO must work first). An unknown value is treated as admins_only. See Single sign-on
QUERYPROXY_OIDC_ISSUER / QUERYPROXY_OIDC_CLIENT_ID — OpenID Connect single sign-on is on when both are set. See Single sign-on
QUERYPROXY_OIDC_CLIENT_SECRET — Client secret; QUERYPROXY_OIDC_CLIENT_SECRET_FILE reads it from a file
QUERYPROXY_OIDC_SCOPES openid email profile Scopes requested from the provider, separated by spaces; some providers need groups added
QUERYPROXY_OIDC_GROUP_CLAIM groups Claim that lists the user’s groups; a dotted path such as realm_access.roles is followed
QUERYPROXY_OIDC_ROLE_MAP — group:team:role entries, comma-separated; team is a slug or name, role is dba, developer or auditor. Applied at every SSO sign-in
QUERYPROXY_SCIM_ENABLED false Serve SCIM 2.0 provisioning under /scim/v2; while off every SCIM path answers 404. See SCIM provisioning
QUERYPROXY_SCIM_RATE_LIMIT 300 SCIM requests per minute for each token; failed authentications are limited per address to the same number
QUERYPROXY_API_TOKEN_MAX_DAYS 90 Longest lifetime of an API personal access token, in days. See REST API and tokens
QUERYPROXY_API_RATE_LIMIT 60 API calls per minute for each token; failed authentications are limited per address to the same number
QUERYPROXY_CHAT_WEBHOOK_ALLOWED_HOSTS — Extra allowed hosts for outbound chat webhooks, comma-separated, * wildcards allowed (SSRF guard; hooks.slack.com, *.webhook.office.com, *.logic.azure.com and *.powerplatform.com are allowed by default)
QUERYPROXY_CHAT_INCLUDE_SQL literals_redacted SQL preview in chat notifications: literals_redacted (string and number literals replaced by ?), full (as submitted) or none. The old booleans work: true is full, false is none
QUERYPROXY_SLACK_CLIENT_ID / QUERYPROXY_SLACK_CLIENT_SECRET — Client credentials of the Slack app, for the OAuth install. See Slack integration
QUERYPROXY_SLACK_SIGNING_SECRET — Signing secret of the Slack app; verifies every request Slack sends
QUERYPROXY_SLACK_RATE_LIMIT 300 Slack requests per minute and workspace, counted after the signature is verified (before it, an address is limited to 120 per minute)
QUERYPROXY_SECRET_CACHE_TTL 300 Seconds a resolved external secret stays in process memory; 0 turns the cache off. See External secret references
QUERYPROXY_SECRET_TIMEOUT 5 Seconds a secret store may take to connect and to answer
QUERYPROXY_VAULT_*, QUERYPROXY_AWS_REGION, QUERYPROXY_AZURE_* — Connection to HashiCorp Vault, AWS Secrets Manager and Azure Key Vault; every variable is listed on External secret references
QUERYPROXY_PENDING_REMIND_AFTER 4 Hours a request may stay pending before one reminder goes to the team’s DBAs and admins, in the portal and in chat. 0 turns the reminder off; requests never expire either way
QUERYPROXY_METRICS_TOKEN — Bearer token for GET /metrics, 24 or more random characters. /metrics answers 404 until this or the allowlist is set. See Monitoring
QUERYPROXY_METRICS_ALLOWED_IPS — Comma-separated addresses or CIDR ranges that may scrape /metrics without a token (follows the TRUSTED_PROXIES rules)
QUERYPROXY_SIEM_SINKS — Audit export sinks to turn on: stdout, syslog, webhook. The QUERYPROXY_SIEM_* settings are listed in Audit chain and SIEM export
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 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).

Read credentials and timeouts

A read request runs in a read-only session on the target database, with a statement timeout and a lock timeout that QueryProxy sets. What that means per driver:

Driver Read-only session Statement timeout Lock timeout
PostgreSQL SET TRANSACTION READ ONLY statement_timeout (and idle_in_transaction_session_timeout) lock_timeout
MySQL SET SESSION TRANSACTION READ ONLY MAX_EXECUTION_TIME, SELECT only innodb_lock_wait_timeout
MariaDB SET SESSION TRANSACTION READ ONLY max_statement_time innodb_lock_wait_timeout
SQL Server none (the engine has no read-only session) PDO query timeout SET LOCK_TIMEOUT
SQLite PRAGMA query_only none busy timeout

The statement timeout is QUERYPROXY_EXECUTION_TIMEOUT minus 5 seconds (at least 1), and the lock timeout is QUERYPROXY_LOCK_TIMEOUT. Writes get the same limits. On MySQL the statement timeout does not apply to writes, so a long write there is not cut by the database; the job ends at the execution timeout and the write may still run on the server. Stop query cancels it earlier.

MySQL and MariaDB set the read-only mode for the whole session, so a statement that commits implicitly cannot slip out of it. On PostgreSQL, SELECT … FOR UPDATE and FOR SHARE are refused inside a read request.

A connection can carry an optional read username and password (encrypted like the main credentials). Reads use them; writes never do. The connection test tries both, and a failure of the read credentials starts with “Read credentials:”. Give the read user SELECT privileges only. On SQL Server this is the only database-side protection for reads, because the engine has no read-only session; ApplicationIntent=ReadOnly does not protect against writes. SQLite connections do not store a read identity.

TLS to target databases

Each network connection has a TLS setting in the connection form. New connections default to required TLS: they fail instead of falling back to plain text.

Driver Options New connection
PostgreSQL require, verify-ca, verify-full, prefer, disable require (encrypted, server not verified). verify-ca and verify-full use the CA you paste
MySQL, MariaDB required or left to the driver default; “Verify the server certificate” required, server certificate not verified
SQL Server required or left to the driver default; “Trust the server certificate” Encrypt=true, TrustServerCertificate=false
SQLite none none

For PostgreSQL, MySQL and MariaDB you can paste a CA certificate, a client certificate and a client key as PEM. They are stored encrypted, written at run time to storage/app/tls/<connection id>/ (directory mode 0700, files 0600) and removed with the connection. The form never sends them back to the browser, and the client key must not be password protected. The SQL Server form has no PEM fields.

require and required without verification encrypt the traffic but do not prove who answers, so they do not stop a man in the middle. Turn verification on where the network is not fully yours.

Connections that exist from before v0.3.0 keep their old behaviour and are marked “TLS not enforced” (PostgreSQL, sslmode=prefer) or “TLS: driver default” in the list; queryproxy:doctor names them. SQL Server requires the pdo_sqlsrv extension for required TLS: without it Laravel would silently fall back to a connection that ignores the encryption setting, so QueryProxy refuses to connect. With Encrypt=true, refusing a server that offers no encryption is the ODBC driver’s documented behaviour; QueryProxy’s tests cannot simulate such a server.

Secrets from files

Docker and Kubernetes secrets arrive as files. For each variable below, set the same name with a _FILE suffix instead, and the entrypoint (and the application) read the value from that file:

Variable File variable
APP_KEY APP_KEY_FILE
APP_PREVIOUS_KEYS APP_PREVIOUS_KEYS_FILE
DB_PASSWORD DB_PASSWORD_FILE
MAIL_PASSWORD MAIL_PASSWORD_FILE
REDIS_PASSWORD REDIS_PASSWORD_FILE
SLACK_BOT_USER_OAUTH_TOKEN SLACK_BOT_USER_OAUTH_TOKEN_FILE
QUERYPROXY_ADMIN_PASSWORD QUERYPROXY_ADMIN_PASSWORD_FILE
QUERYPROXY_METRICS_TOKEN QUERYPROXY_METRICS_TOKEN_FILE
QUERYPROXY_SIEM_WEBHOOK_SECRET QUERYPROXY_SIEM_WEBHOOK_SECRET_FILE
QUERYPROXY_OIDC_CLIENT_SECRET QUERYPROXY_OIDC_CLIENT_SECRET_FILE
QUERYPROXY_VAULT_TOKEN QUERYPROXY_VAULT_TOKEN_FILE
QUERYPROXY_VAULT_SECRET_ID QUERYPROXY_VAULT_SECRET_ID_FILE
QUERYPROXY_AZURE_CLIENT_SECRET QUERYPROXY_AZURE_CLIENT_SECRET_FILE
services:
  app:
    environment:
      APP_KEY_FILE: /run/secrets/queryproxy_app_key
      DB_PASSWORD_FILE: /run/secrets/queryproxy_db_password
      QUERYPROXY_METRICS_TOKEN_FILE: /run/secrets/queryproxy_metrics_token
    secrets: [queryproxy_app_key, queryproxy_db_password, queryproxy_metrics_token]

In Kubernetes, point the _FILE variable at the path where a Secret volume is mounted.

  • Setting both X and X_FILE is an error and the container does not start. A plain variable set to null, (null) or (empty) counts as unset.
  • The file must exist, be readable and not be empty. Trailing line breaks are removed; everything else is kept.
  • Error messages name the variable and the path, never the value.
  • php artisan config:cache (the image runs it on every start) writes the resolved values, secrets included, in plain text to bootstrap/cache/config.php. A secret that came from a file therefore also lives in that cache file.
  • The signing secrets and webhook URLs of the incoming-webhook mode of Slack and Teams are not environment variables; they are stored encrypted in the application database. The Slack app’s own credentials (QUERYPROXY_SLACK_CLIENT_SECRET, QUERYPROXY_SLACK_SIGNING_SECRET) are plain environment variables and have no _FILE form.

php artisan queryproxy:doctor reports which secrets are read from files (secret_files) and fails when one cannot be read. See Troubleshooting.

Single sign-on

Single sign-on through OpenID Connect is configured with the QUERYPROXY_OIDC_* variables in the table above. Register {APP_URL}/auth/oidc/callback as the redirect URI at the provider. The provider examples, the role map and the rules for creating and linking accounts are on Single sign-on. The client secret can come from a file (QUERYPROXY_OIDC_CLIENT_SECRET_FILE), like the other secrets in Secrets from files.

Schema explorer

The Query Studio’s schema panel and name completion read the target database’s catalog through fixed queries, in a read-only session, without an approval. Only users who may use the connection can read it, and the answer lists tables, views and columns, never data.

  • QUERYPROXY_SCHEMA_TIMEOUT is the statement timeout of that read, in seconds. It is separate from the execution timeout of requests. A read also needs a free connection slot (QUERYPROXY_CONNECTION_SLOTS); with every slot busy the panel asks the user to try again shortly.
  • The result is cached per connection for QUERYPROXY_SCHEMA_CACHE_TTL seconds, and editing the connection starts a fresh entry. A manual Refresh skips the cache and is limited per user and connection to QUERYPROXY_SCHEMA_REFRESH_LIMIT a minute. The cache holds names and types only; the lock on masked columns is computed from the current masking rules on every answer.
  • QUERYPROXY_SCHEMA_MAX_COLUMNS caps one read. A larger schema is cut at a table boundary with the default schema listed first, and the panel says it is truncated.
  • On MySQL and MariaDB only the connection’s own database is listed. The system schemas (information_schema, mysql, performance_schema, sys) are never listed.
  • Each read from the database is recorded as schema.read in the audit log, with the object and column counts. A read served from the cache is not.

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:

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:

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, MariaDB, PostgreSQL or SQL Server, set the standard Laravel variables:

DB_CONNECTION=pgsql        # or mysql, mariadb, sqlsrv
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.

Moving from SQLite to another database. Stop QueryProxy, then run php artisan queryproxy:migrate-appdb --to=<dsn>, for example --to=pgsql://user:password@host:5432/queryproxy (URL-encode special characters in the password). It runs the migrations on the target, copies every table in foreign key order, compares the row count of every table and runs audit:verify on the target. The audit triggers stay in place and the audit hashes and the chain head are copied unchanged. The target must be empty (a database that was only migrated is fine). --dry-run checks everything and writes nothing; without --force the command asks you to confirm that QueryProxy is stopped. The copy is one transaction, so a failure leaves a migrated, empty target you can retry. The cache tables are not copied, and the target password is never printed. Afterwards switch DB_CONNECTION and the other DB_* settings and start QueryProxy again.

SQL Server as the application database. Use the mssql image variant: the default image has no pdo_sqlsrv (see SQL Server targets). Move an installation there with php artisan queryproxy:migrate-appdb --to="sqlsrv://user:password@host:1433/queryproxy?trust_server_certificate=true". Options in the query string, such as trust_server_certificate and encrypt, go to the connection of that command only. The application has no separate DB_* variable for them, so after the move give it the same DSN, with the same options, in DB_URL; otherwise a server with a self-signed certificate is reachable for migrate-appdb but not for the running application. DB_URL carries the password, so URL-encode special characters in it and keep it as secret as DB_PASSWORD. SQL Server refuses foreign keys that give a table more than one cascade path, and the schema has several. So on SQL Server only, eight foreign keys are created with ON DELETE NO ACTION: query_requests.team_id, masking_rules.team_id, connection_grant_requests.team_id, connections.created_by, connection_user.granted_by, connection_grant_requests.decided_by, and the two query_requests keys to users (user_id and reviewed_by), which are RESTRICT on every driver, so that request history is never deleted with a user (ADR-018). QueryProxy does what the database would have done on a delete: deleting a team removes its requests, masking rules and grant requests first, and hard-deleting a user who has no requests sets the keys that referenced them to NULL.

What differs is raw SQL that deletes a team or a user. On SQL Server it is refused only while rows still point at the team or user through one of those eight keys: for a team, requests, masking rules or grant requests; for a user, connections it created, grants it gave, grant requests it decided, or requests it filed or reviewed. For the six keys that cascade or set NULL on the other drivers, the delete is refused on SQL Server instead. The two request keys to users are refused on every driver. Nothing changes on PostgreSQL, MySQL, MariaDB or SQLite: no new migration, no change to an existing installation, the same delete results.

Tested versions. Target databases: PostgreSQL 17, MySQL 8.4, MariaDB 11.4 and 11.8, SQL Server 2022 and SQLite. Application database: SQLite, MySQL 8.4, MariaDB 11.4 and 11.8, PostgreSQL 17, and SQL Server 2022 (the mssql image variant). PostgreSQL-compatible target databases: CockroachDB v25.4 and YugabyteDB 2025.2, each with known differences listed on PostgreSQL-compatible databases, which includes what the SQL guard does not cover on CockroachDB. Other versions may work but are not tested.

Queue

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

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):

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. For Microsoft SQL Server targets use the mssql image variant: queryproxy/queryproxy:<version>-mssql, or queryproxy/queryproxy:mssql for the latest stable release. It adds Microsoft’s ODBC Driver 18 and the pdo_sqlsrv extension, is built for linux/amd64 and linux/arm64, and is scanned, signed and attested like the default image. :mssql moves only with stable releases and never takes latest. Microsoft’s driver is under Microsoft’s own licence (https://aka.ms/odbc18eula); using the variant means accepting it.

To build the variant yourself, pass --build-arg INSTALL_SQLSRV=1 to the repository’s Dockerfile. 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: run the same variant for every role.

Privileges for Stop query

Stop query cancels the running statement from a second connection. It needs no extra privilege on PostgreSQL, MySQL and MariaDB beyond what the connection’s user has (pg_signal_backend or CONNECTION_ADMIN suffice for other users’ sessions). On SQL Server the connection’s login needs:

  • VIEW SERVER STATE: without it sys.dm_exec_sessions shows only the caller’s own session, the run cannot be found, and the request is marked cancelled without signalling the server (a warning appears and the statement ends at its timeout).
  • ALTER ANY CONNECTION: KILL <spid> needs it.

On SQL Server, Stop ends the session, so an open transaction rolls back on the server; on the other drivers only the statement is cancelled. The button is not shown for SQLite.

After a statement has started, QueryProxy does not reconnect on a network drop; the run fails instead of continuing on a new session.

Arrow keys to move, Enter to open.