Skip to content

Configuration

pgvis configuration is layered (later sources override earlier):

  1. Defaults — sensible production defaults from Config::default()
  2. TOML config file — specified via --config flag or PGVIS_CONFIG env var
  3. Environment variables — prefixed with PGVIS_, lowercase field names
VariablePurposeDefault
PGVIS_DSNDatabase connection stringrequired
PGVIS_BINDServer bind address0.0.0.0:3000
PGVIS_CONFIGPath to TOML config filenone
PGVIS_SCHEMASSchemas to expose (comma-separated)public
PGVIS_JWT_SECRETJWT verification secretnone (anonymous only)
PGVIS_JWT_ALGOJWT algorithmHS256
PGVIS_ANON_ROLEDatabase role for unauthenticated requestsnone
PGVIS_ROLE_CLAIM_KEYJWT claim key for rolerole
PGVIS_MAX_ROWSMaximum rows returned per requestunlimited
PGVIS_STATEMENT_TIMEOUT_MSSQL statement timeout (ms)30000
PGVIS_POOL_SIZEPool max connections16
PGVIS_POOL_TIMEOUT_MSPool checkout timeout (ms)5000
PGVIS_POOL_CREATE_TIMEOUT_MSPool create connection timeout (ms)5000
PGVIS_POOL_RECYCLE_TIMEOUT_MSPool recycle/validate timeout (ms)5000
PGVIS_POOL_KEEPALIVESTCP keepalive on connectionstrue
PGVIS_POOL_KEEPALIVES_IDLE_SECSKeepalive idle time (seconds)60
PGVIS_POOL_CONNECT_TIMEOUT_SECSTCP connect timeout (seconds)10
PGVIS_POOL_RECYCLING_METHODConnection recycling methodFast
PGVIS_READ_ONLYReject all mutationsfalse
PGVIS_AGGREGATES_ENABLEDAllow aggregate functions in selectfalse
PGVIS_PLAN_ENABLEDAllow EXPLAIN plan requestsfalse
PGVIS_TX_ALLOW_OVERRIDEAllow Prefer: tx=rollbackfalse
PGVIS_TX_ROLLBACK_ALLForce rollback on all transactionsfalse
PGVIS_PRE_REQUESTPre-request function namenone
PGVIS_OPENAPI_TITLEOpenAPI document titlenone
PGVIS_OPENAPI_SERVER_URLOpenAPI server URL overridenone
PGVIS_REPLICA_DSNSRead replica DSNs (comma-separated)none
PGVIS_MAX_REPLICATION_LAG_BYTESReplica lag threshold in bytes10485760 (10 MB)
PGVIS_HEALTH_CHECK_INTERVAL_MSReplica health check interval (ms)5000
PGVIS_CACHE_ENABLEDEnable the in-memory read cachefalse
PGVIS_CACHE_TTLCache entry TTL in seconds60
PGVIS_CACHE_MAX_ENTRIESMax cache entries (LRU capacity)10000
PGVIS_CACHE_LISTSAlso cache list queries (not just PK lookups)false
RUST_LOGLog level filterpgvis=info,tower_http=info

Create a pgvis.toml file and reference it with --config pgvis.toml or PGVIS_CONFIG=pgvis.toml:

# Schema selection
schemas = ["public", "api"]
extra_search_path = ["extensions"]
# Authentication
jwt_secret = "your-secret-key-here"
jwt_algo = "HS256"
anon_role = "web_anon"
role_claim_key = "role"
# Feature gates
aggregates_enabled = true
plan_enabled = false
tx_allow_override = true
tx_rollback_all = false
read_only = false
# Query limits
max_rows = 1000
statement_timeout_ms = 30000
# Connection pool
[pool]
size = 16
timeout_ms = 5000
create_timeout_ms = 5000
recycle_timeout_ms = 5000
keepalives = true
keepalives_idle_secs = 60
connect_timeout_secs = 10
recycling_method = "Fast"
# Hooks
pre_request = "auth.check_request"
# OpenAPI
openapi_title = "My API"
openapi_server_url = "https://api.example.com"
openapi_mode = "IgnorePrivileges"
# Routing
[routing]
prefix = "api"
schema_in_path = true
default_schema = "public"
mcp_separator = "/"
# Read replicas (Postgres only)
[replica]
replica_dsns = [
"postgres://replica1:5432/mydb",
"postgres://replica2:5432/mydb",
]
max_replication_lag_bytes = 10485760 # 10 MB
health_check_interval_ms = 5000
primary_reads = true
# In-memory read cache (off by default)
[cache]
enabled = true
ttl_seconds = 60
max_entries = 10000
cache_lists = false

FieldTypeDefaultDescription
schemasstring[]["public"]Which schemas to expose as API endpoints. Tables, views, and functions in these schemas become REST routes and MCP tools.
extra_search_pathstring[][]Additional schemas for type/function resolution (not exposed as endpoints).
FieldTypeDefaultDescription
jwt_secretstring?nullJWT secret for token verification. If absent, JWT verification is disabled. Can be a symmetric secret or path to a public key.
jwt_algoenumHS256JWT signing algorithm. One of: HS256, HS384, HS512, RS256, EdDSA.
anon_rolestring?nullThe database role used for unauthenticated requests. On Postgres, this role’s permissions define anonymous access.
role_claim_keystring"role"The JWT claim key that specifies the database role. The value of this claim becomes the SET LOCAL role value.
  1. Client sends Authorization: Bearer <token> header
  2. pgvis verifies the token against jwt_secret using jwt_algo
  3. The role from the token’s role_claim_key claim is used to SET LOCAL role
  4. All JWT claims are available as current_setting('request.jwt.claims') in SQL
  5. This integrates with PostgreSQL Row-Level Security (RLS)

If no valid JWT is present, the anon_role is used (or the connection’s default role if anon_role is not set).

FieldTypeDefaultDescription
aggregates_enabledboolfalseWhether aggregate functions (sum, avg, count, min, max) are allowed in select. Disabled by default because aggregates can be expensive without proper indexes.
plan_enabledboolfalseWhether clients can request Accept: application/vnd.pgrst.plan+json to get the query execution plan.
tx_allow_overrideboolfalseWhether Prefer: tx=rollback is honoured. When false, the preference is silently ignored.
tx_rollback_allboolfalseForce rollback of ALL transactions regardless of client preference. Useful for testing/staging environments.
read_onlyboolfalseReject all mutations (POST/PATCH/DELETE on tables, VOLATILE RPC calls). MCP tool catalogues omit write tools entirely.
FieldTypeDefaultDescription
max_rowsu64?null (unlimited)Maximum number of rows returned per request. Acts as a server-side cap on limit. When a client requests more rows than this (or no limit), the response is capped.
statement_timeout_msu64?30000 (30s)SQL statement timeout in milliseconds. Applied to every query. Prevents runaway queries. Set to null or 0 to disable.

Pool settings live under the [pool] table in TOML (or as flat PGVIS_POOL_* env vars). These settings apply to both the primary pool and all replica pools.

FieldTypeDefaultDescription
pool.sizeu3216Maximum number of database connections in the pool. Each concurrent request holds one connection for its transaction duration. For replicas, each replica pool gets this many connections independently.
pool.timeout_msu645000 (5s)Pool checkout timeout. If all connections are busy, new requests wait up to this duration before returning 503 Service Unavailable. Set to 0 for no timeout (not recommended).
pool.create_timeout_msu645000 (5s)Timeout for creating a new connection. Bounds how long establishing a new TCP connection + TLS handshake + Postgres auth can take. Set to 0 for no timeout.
pool.recycle_timeout_msu645000 (5s)Timeout for recycling/validating an existing connection. When a pooled connection is checked out, the recycling query must complete within this time or the connection is discarded. Set to 0 for no timeout.
pool.keepalivesbooltrueWhether TCP keepalive is enabled on connections. Critical for connections through NATs, firewalls, or load balancers that silently drop idle TCP connections.
pool.keepalives_idle_secsu6460 (1 min)How long a connection must be idle before the first keepalive probe is sent. Lower values detect dead connections faster but generate more traffic. Only effective when keepalives = true.
pool.connect_timeout_secsu6410Maximum time (seconds) to wait for a TCP connection to be established with the Postgres server. Independent of the pool checkout timeout. Set to 0 for no timeout.
pool.recycling_methodenumFastHow connections are validated when returned to the pool. Fast: check is_closed() only (cheapest). Verified: execute an empty query to ensure the connection responds (one round-trip). Clean: run DISCARD-like statements for pristine session state (most thorough).
MethodOverheadUse when
FastNone (in-process check)Low-latency is critical; Postgres/network is stable
Verified1 round-tripConnections cross unreliable networks or NATs
CleanSeveral round-tripsUntrusted extensions or session state leaks between requests
  • Behind a load balancer/NAT: Set keepalives = true and keepalives_idle_secs below the LB idle timeout (often 60–300 s).
  • Serverless/burst traffic: Lower create_timeout_ms (e.g. 3000) so cold-start pool growth doesn’t block the first requests too long.
  • High-availability: Use recycling_method = "Verified" to detect dead connections immediately on checkout rather than failing the first query.
FieldTypeDefaultDescription
replica.replica_dsnsstring[][]DSNs of read replicas. When non-empty, enables lag-aware read routing and load balancing across replicas.
replica.max_replication_lag_bytesu6410485760 (10 MB)Maximum acceptable replication lag in bytes. Replicas exceeding this are excluded from the read pool. Set to 0 to disable lag checking.
replica.health_check_interval_msu645000 (5s)How often the health monitor checks replica lag and connectivity.
replica.primary_readsbooltrueWhether the primary also serves read queries. When false, reads go exclusively to replicas (primary handles only writes). Falls back to primary if all replicas are unhealthy.
  1. Writes (POST, PATCH, PUT, DELETE on tables) always go to the primary
  2. Reads (GET on tables/views, read-only RPC) are distributed round-robin across healthy replicas (and the primary if primary_reads = true)
  3. A background health monitor checks each replica’s replication lag every health_check_interval_ms
  4. Replicas whose WAL receive position is more than max_replication_lag_bytes behind the primary are temporarily excluded
  5. If all replicas are unhealthy, reads fall back to the primary automatically

pgvis does not handle promotion. External tools (Patroni, pg_auto_failover, DNS/VIP) handle that. When the primary becomes unreachable:

  • Active connections fail with a pool error (503 to the client)
  • Once the external tool updates DNS/VIP, the next pool checkout resolves to the new primary
  • No manual restart required — deadpool-postgres evicts broken connections automatically

An optional in-memory cache for read responses. Off by default — when disabled it allocates nothing and adds no per-request cost.

FieldTypeDefaultDescription
cache.enabledboolfalseMaster switch. When true, an in-memory response cache is created at startup.
cache.ttl_secondsu6460How long a cached entry lives before it expires and the next read re-queries the database. Lower = fresher data; higher = less DB load.
cache.max_entriesu6410000Maximum entries held. When full, least-recently-used entries are evicted.
cache.cache_listsboolfalseWhether to cache list/collection queries in addition to primary-key lookups. See the caveats below before enabling.
  • PK lookups (every primary-key column filtered with eq) are cached whenever the cache is enabled.
  • List queries are cached only when cache_lists = true.
  • Queries with embeds are never cached.

The cache key hashes the request’s JWT claims together with the rendered SQL and parameters, prefixed by the database role. This means each entry is unique per (role, identity, query shape):

  • Two users who share a role but differ in identity (e.g. different sub) get separate entries — one user’s row-level-security-filtered rows are never served to another.
  • The same primary key requested with a different select, an extra filter, a different order, or different pagination gets a separate entry — the cache never returns a response of the wrong shape.

Any write clears the entire cache:

  • A mutation (POST/PATCH/PUT/DELETE) on any table, and
  • A volatile RPC call (a function that may modify data).

Clearing wholesale is deliberate: a write can cascade to other tables through foreign keys or triggers, so evicting only the directly-targeted table could leave stale rows elsewhere. The ttl_seconds window is the upper bound on staleness for everything else (for example, a data-modifying function mislabeled STABLE/IMMUTABLE in the catalog, which won’t trigger invalidation).

  • Highly personalized data caches poorly. Because claims are part of the key, per-user data produces a distinct entry per user and gets little benefit. The cache shines on PK lookups against shared, role-gated data.
  • List cardinality. High-variety list traffic produces many distinct keys, yielding low hit rates and high memory churn. This is why cache_lists is off by default.
  • Write-heavy multi-table workloads. Since every write clears the whole cache, frequent writes erase the working set before it pays off. The cache targets read-heavy workloads.

GET /pgvis/cache (a fixed path, not under your configured prefix) returns the current settings plus hit/miss/invalidation counters and the hit rate:

{
"settings": { "enabled": true, "ttl_seconds": 60, "max_entries": 10000, "cache_lists": false },
"stats": { "hits": 1280, "misses": 240, "invalidations": 12, "entries": 305, "hit_rate": 84.21 }
}

The endpoint is always available; stats is null when caching is disabled.

FieldTypeDefaultDescription
pre_requeststring?nullFully-qualified function name to call before every query. Called after JWT verification and role switching. Can raise exceptions to abort the request (e.g., rate limiting, audit logging).
FieldTypeDefaultDescription
openapi_titlestring?nullTitle in the OpenAPI document’s info section.
openapi_server_urlstring?nullServer URL override for the OpenAPI spec (for proxied deployments).
openapi_modeenumIgnorePrivilegesControls spec generation. IgnorePrivileges: show everything. FollowPrivileges: filter based on role. Disabled: disable the endpoint entirely.
FieldTypeDefaultDescription
routing.prefixstring"api"URL prefix for all API routes. Leading/trailing slashes stripped. Set to "" for PostgREST-compatible flat routes.
routing.schema_in_pathbooltrueWhether the schema name appears in the URL path. When true: /{prefix}/{schema}/{table}. When false: /{prefix}/{table} with schema from header.
routing.default_schemastring"public"The default schema when schema_in_path = false and no profile header is sent. Must be one of the schemas listed in schemas.
routing.mcp_separatorchar'/'Separator in MCP tool names. / maps to hierarchical discovery. . is an alternative for flat tool lists.

[routing]
prefix = "api"
schema_in_path = true

Routes:

GET /api/public/users
GET /api/analytics/events
POST /api/public/rpc/search
[routing]
prefix = "api"
schema_in_path = false
default_schema = "public"

Routes:

GET /api/users
POST /api/rpc/search

PostgREST-compatible (drop-in replacement)

Section titled “PostgREST-compatible (drop-in replacement)”
[routing]
prefix = ""
schema_in_path = false
default_schema = "public"

Routes:

GET /users
POST /rpc/search

CLI flags override config file and environment variable values.

FlagEnvDescription
--bind <ADDR>PGVIS_BINDBind address (default: 0.0.0.0:3000)
--schema <NAME>PGVIS_SCHEMASSchemas to expose (repeatable or comma-separated)
--replica-dsn <DSN>PGVIS_REPLICA_DSNSRead replica DSNs (repeatable or comma-separated)
--cache-enabledPGVIS_CACHE_ENABLEDEnable the in-memory read cache
--cache-ttl <SECS>PGVIS_CACHE_TTLCache entry TTL in seconds
--cache-max-entries <N>PGVIS_CACHE_MAX_ENTRIESMax cache entries (LRU capacity)
--cache-listsPGVIS_CACHE_LISTSAlso cache list queries (not just PK lookups)
--mcp-httpAlso serve MCP at /mcp via Streamable HTTP
FlagEnvDescription
--schema <NAME>PGVIS_SCHEMASSchemas to expose
--read-onlyExpose only read tools (no create/update/delete)
FlagEnvDescription
--dsn <DSN>PGVIS_DSNDatabase connection string (required)
--config <FILE>PGVIS_CONFIGPath to TOML config file

When the same setting is specified in multiple places, the highest-priority source wins:

CLI flags > Environment variables > TOML config file > Defaults

Example:

Terminal window
# Config file sets max_rows = 1000
# Env var overrides: PGVIS_MAX_ROWS=500
# Result: max_rows = 500
PGVIS_MAX_ROWS=500 pgvis --config pgvis.toml serve

FieldTypeDefaultDescription
pubsub.enabledboolfalseEnable the pub/sub subsystem
pubsub.channel_prefixstring"pgvis:"Prefix added to Postgres LISTEN/NOTIFY channel names
pubsub.max_payload_bytesinteger7500Maximum message payload size
pubsub.buffer_capacityinteger256In-process broadcast buffer per channel
pubsub.allowed_channelsstring[][]Channel allowlist (empty = allow all). Supports globs: chat.*

TOML example:

[pubsub]
enabled = true
channel_prefix = "myapp:"
max_payload_bytes = 7500
allowed_channels = ["chat.*", "notifications", "events.*"]

Environment variables:

VariableMaps to
PGVIS_PUBSUB_ENABLEDpubsub.enabled
PGVIS_PUBSUB_CHANNEL_PREFIXpubsub.channel_prefix

See the Pub/Sub guide for full usage details.


PostgREST configpgvis equivalent
db-schemasschemas
db-extra-search-pathextra_search_path
db-anon-roleanon_role
jwt-secretjwt_secret
jwt-role-claim-keyrole_claim_key
db-aggregates-enabledaggregates_enabled
db-plan-enabledplan_enabled
db-tx-allow-overridetx_allow_override
db-tx-rollback-alltx_rollback_all
max-rowsmax_rows
db-pre-requestpre_request
openapi-server-proxy-uriopenapi_server_url
openapi-modeopenapi_mode