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; nested tables use __ (double underscore), e.g. PGVIS_POOL__SIZE → pool.size
  4. CLI flags — e.g. --schema, --cache-ttl, --read-only

Each layer overrides only the keys it sets, so a key missing from the TOML file and the environment keeps its default:

flowchart LR
    d["<b>Defaults</b>"] --> t["<b>TOML</b>"] --> e["<b>Env</b>"] --> c["<b>CLI</b>"]

The config file is strict: a missing file, or any key pgvis does not recognise (a typo, or a PostgREST spelling such as jwt-secret), stops startup with an error that names the offending keys. Environment variables are not checked this way — an unrecognised PGVIS_* variable is silently ignored, so double-check the spelling (in particular the __ for nested keys).

These are read by the CLI itself (see CLI Flags).

VariablePurposeDefault
PGVIS_DSNDatabase connection stringrequired
PGVIS_CONFIGPath to TOML config filenone
PGVIS_SCHEMASSchemas to expose (comma-separated; all commands)public (SQLite tables live in main, so pass main there)
PGVIS_BINDserve bind address127.0.0.1:3000
PGVIS_INSECURE_NO_AUTHserve: allow starting with neither jwt_secret nor anon_role (true)false
PGVIS_REPLICA_DSNSserve: read replica DSNs (comma-separated)none
PGVIS_CACHE_ENABLEDserve: enable the in-memory read cachefalse
PGVIS_CACHE_TTLserve: cache entry TTL in seconds60
PGVIS_CACHE_MAX_ENTRIESserve: max cache entries10000
PGVIS_CACHE_LISTSserve: also cache list queries (not just PK lookups)false
PGVIS_PUBSUB_ENABLEDserve: enable pub/subfalse
PGVIS_PUBSUB_CHANNEL_PREFIXserve: pub/sub channel prefixpgvis:
RUST_LOGLog level filter (logs are JSON on stderr)pgvis=info,tower_http=info
SSL_CERT_FILECA bundle used to verify the Postgres server’s TLS certificate (see TLS)platform trust store

Every scalar config key can also be set as PGVIS_<KEY> (top-level keys) or PGVIS_<TABLE>__<KEY> (keys inside a [table]). List-valued keys other than schemas (extra_search_path, replica.replica_dsns, pubsub.allowed_channels) are best set in the TOML file.

VariableConfig keyDefault
PGVIS_JWT_SECRETjwt_secretnone
PGVIS_JWT_ALGOjwt_algoHS256
PGVIS_JWT_AUDjwt_audnone (aud not checked)
PGVIS_ANON_ROLEanon_rolenone
PGVIS_ROLE_CLAIM_KEYrole_claim_keyrole
PGVIS_AGGREGATES_ENABLEDaggregates_enabledfalse
PGVIS_PLAN_ENABLEDplan_enabledfalse
PGVIS_TX_ALLOW_OVERRIDEtx_allow_overridefalse
PGVIS_TX_ROLLBACK_ALLtx_rollback_allfalse
PGVIS_MAX_ROWSmax_rowsunlimited
PGVIS_STATEMENT_TIMEOUT_MSstatement_timeout_ms30000
PGVIS_READ_ONLYread_onlyfalse
PGVIS_PRE_REQUESTpre_requestnone
PGVIS_OPENAPI_TITLEopenapi_titlenone
PGVIS_OPENAPI_SERVER_URLopenapi_server_urlnone
PGVIS_OPENAPI_MODEopenapi_modeIgnorePrivileges

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"
jwt_aud = "my-api" # optional: require this `aud` claim
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?unsetKey used to verify tokens. For HS256/HS384/HS512 it is the shared secret (used as raw bytes). For RS256/EdDSA it is the PEM-encoded public key itself (the key text, not a file path); a key that fails to parse makes every authenticated request fail with a 500 rather than falling back to HMAC. If unset, JWT verification is disabled and every request runs as anon_role.
jwt_algoenumHS256JWT signing algorithm. One of: HS256, HS384, HS512, RS256, EdDSA.
jwt_audstring?unsetRequired audience. When set, a token whose aud does not include this value is rejected (401). When unset, aud is not checked. Set it whenever the signing key is shared with other applications.
anon_rolestring?unsetThe database role used for unauthenticated requests, and for tokens without a role claim. On Postgres, this role’s permissions define anonymous access. On SQLite it is informational only.
role_claim_keystring"role"The (top-level) JWT claim that names the database role. Its value becomes the transaction’s role.
  1. Client sends Authorization: Bearer <token> header
  2. pgvis verifies the token against jwt_secret using jwt_algo. The signature is always checked; exp and nbf are checked when present; aud is checked when jwt_aud is set
  3. The role from the token’s role_claim_key claim becomes the transaction’s role (set_config('role', …, true)). A token without that claim falls back to anon_role; if no anon_role is configured it is rejected with 401 — it never runs as the DSN’s own role
  4. All JWT claims are available as one JSON value in current_setting('request.jwt.claims', true) in SQL. (The older per-claim request.jwt.claim.<key> settings are not set — read claims from the JSON, e.g. current_setting('request.jwt.claims', true)::json->>'sub'.)
  5. This integrates with PostgreSQL Row-Level Security (RLS)

The role a request runs as is decided before any query planning. Every path either ends at a role or at a 401; with jwt_secret set, none of them falls through to the DSN’s own role. “Valid” means the signature verifies, exp/nbf pass, and aud matches when jwt_aud is set:

flowchart TB
    req(["Request"]) --> secret{"jwt_secret?"}
    secret -->|no| noauth["<b>No verification</b><br/>anon_role, or the DSN role if unset"]
    secret -->|yes| tok{"token?"}
    tok -->|no| anon{"anon_role?"}
    tok -->|yes| valid{"valid?"}
    valid -->|no| e401(["401"])
    valid -->|yes| claim{"role claim?"}
    claim -->|yes| role["<b>Claimed role</b><br/>claims set"]
    claim -->|no| anon
    anon -->|yes| anonrole["<b>anon_role</b><br/>claims set if a token was sent"]
    anon -->|no| e401

Requests without a token run as anon_role. When jwt_secret is set and there is no anon_role, an unauthenticated request is a 401 — this also applies to the metadata endpoints (the OpenAPI/schema root and GET /pgvis/cache).

When jwt_secret is unset, tokens are not verified and every request runs as anon_role. If neither jwt_secret nor anon_role is set, every request would run as the DSN’s own role (often the table owner, which bypasses RLS), so pgvis serve refuses to start unless you pass --insecure-no-auth (or PGVIS_INSECURE_NO_AUTH=true). Use that only for local development.

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_onlyboolfalseMCP read-only mode (also set by pgvis mcp --read-only). The MCP tool catalogue omits create/update/delete tools and all RPC tools, such calls are rejected if attempted anyway, and the pubsub_publish tool is refused. It does not restrict the REST API — use database privileges for that.
FieldTypeDefaultDescription
max_rowsu64?unset (unlimited)Maximum number of rows a read returns. Acts as a server-side cap on limit: when a client requests more rows than this (or no limit), the response is capped. It never limits which rows a PATCH/DELETE changes.
statement_timeout_msu64?30000 (30s)SQL statement timeout in milliseconds, set per transaction (statement_timeout). Prevents runaway queries. Set to 0 to disable.

Pool settings live under the [pool] table in TOML (or as PGVIS_POOL__* env vars, with a double underscore). These settings apply to both the primary pool and all replica pools.

Each request takes one pooled connection for two round trips, however many claims its JWT carries: BEGIN, the role/claims/statement-timeout settings, the pre-request call and the query go out as one pipelined flight, then COMMIT/ROLLBACK. When no connection becomes free within pool.timeout_ms, or the database is unreachable, the request fails with 503 (PGRST000) so clients can retry. GET /pgvis/cache reports the primary pool’s occupancy under pool (max_size, size, available, waiting); a waiting count that stays above zero means the pool is too small for the load.

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 checked out of 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) and calls to VOLATILE functions always go to the primary
  2. Eligible reads (local base tables without RLS, including safe FK joins, and STABLE/IMMUTABLE RPC) are distributed round-robin across healthy replicas. Views, foreign tables, unlogged/temporary tables, ordinary inheritance parents, RLS, computed relationships, explicit select casts/aggregates and unknown table metadata stay on the primary. A configured pre_request hook always stays on the primary.
  3. A background health monitor checks every replica concurrently each health_check_interval_ms, over its own dedicated connections with short timeouts — so a replica whose request pool is merely busy is not marked unhealthy, and one unreachable host doesn’t stall the checks of the others
  4. Replicas whose WAL replay position is more than max_replication_lag_bytes behind the primary are temporarily excluded
  5. If no eligible reader can be checked out, reads fall back to the primary. Query execution errors are never replayed. Eligible read transactions use BEGIN READ ONLY, including primary readers and checkout fallback; lag-checked and discovered readers additionally assert pg_is_in_recovery() inside that transaction, protecting pooled sessions during promotion.

Function volatility is a routing hint, not proof of all side effects: a STABLE wrapper can call a VOLATILE function. READ ONLY blocks writes to non-temporary tables, but cannot sandbox session-local or external effects of privileged extensions. Replica reads are eventually consistent. For a direct Backend::execute call, set ExecContext.require_primary = true when primary consistency is required; replica lag bounds do not guarantee read-after-write. Schema/security changes need schema reload and WAL replay before replica behavior converges. Function volatility cannot reveal dependencies on unlogged data: STABLE RPCs that read unlogged tables require explicit primary execution too. Temporary/session-local data additionally requires connection affinity, which replica routing does not provide.

Automatic physical standby discovery (opt-in)

Section titled “Automatic physical standby discovery (opt-in)”

Static replica_dsns continue to work. Set [replica.discovery] to discover direct physical streaming standbys from the primary’s pg_stat_replication:

[replica]
primary_reads = false
health_check_interval_ms = 1000
max_replication_lag_bytes = 10485760
[replica.discovery]
trusted_cidrs = ["10.20.0.0/24"]
port = 5432
max_nodes = 8
refresh_interval_ms = 5000
stale_after_ms = 30000
# allow_insecure = false # keep false outside isolated development fixtures

The primary DSN must use sslmode=require. Discovered connections preserve its authentication, TLS and session options while replacing all hosts/addresses/ports and setting target_session_attrs=read-only. The replication client port is never a database listening port. port defaults to the primary’s single port (or 5432); a primary DSN with differing ports requires an explicit discovery port.

client_addr must be the standby’s directly reachable query address. A validated client_hostname, when available, supplies the TLS name while hostaddr pins the actual connection to the observed IP; otherwise the certificate needs that IP’s SAN. Only allow CIDRs whose endpoints you trust to receive the primary DSN’s credentials. Cluster identity verification happens after authentication and does not replace this trust boundary or TLS verification. Narrow CIDRs and appropriate DB credentials matter. Discovery does not find cascading standbys and does not support NAT, multiple query ports behind one source IP, Unix replication sockets, or logical subscribers; use static DSNs for those topologies. Ambiguous/masked or untrusted physical membership fails closed.

The connection role needs effective pg_read_all_stats membership (or equivalent superuser/pg_monitor access) plus EXECUTE on pg_control_system() and pg_control_checkpoint(). It needs visibility of pg_stat_wal_receiver on each standby. Grant only these monitoring privileges to the connection role, separately from request roles. Each discovered node must be in recovery, share the primary’s system identifier and current receiving timeline, and meet the replay-lag limit. Setting lag bytes to zero disables only the byte limit for discovery; identity, timeline and recovery checks remain mandatory. Static DSNs retain their explicit connectivity-only mode when lag bytes are zero.

Refresh/probes have bounded deadlines and run sequential refresh rounds. Errors disable discovered routing immediately; expiry also prevents routing while a refresh is delayed. After stale_after_ms without a successful refresh, retained pools retire. Successful removal retires immediately: queued checkouts and new dials stop, while checked-out reads finish. Retiring entries occupy max_nodes until drained, so repeated topology changes cannot multiply connection capacity. Unchanged endpoints reuse pools. Budget request connections as pool.size × (1 + static replicas + discovery.max_nodes), plus one probe per endpoint, schema listener, and any enabled pub/sub connections, per process. Static readers, discovery capacity and an optional primary reader together cannot exceed 64. Discovery does not provide cross-process pooling or promote a primary.

GET /pgvis/cache and backend status methods expose reader identity, source, eligibility/reason, retiring state and pool counters; discovery adds refresh success/failure counts, last-success age and active/retiring counts. Discovered identities contain system identifier/address/port, never credentials or DSNs. Counters describe a pool lifetime and reset when a drained endpoint is recreated.

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

  • Checkout failures report unavailability (503); an executing query can fail with an uncertain outcome and is never replayed
  • 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, entries are evicted (SIEVE policy, which removes stale-generation entries before live ones).
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 (a keyed hash with a random per-process key), prefixed by the database role and the table’s current generation. This means each entry is unique per (role, identity, query shape, table generation):

  • 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.

Invalidation is generation-based — nothing is scanned or cleared; a bumped generation simply makes later reads compute a different key, and old entries age out:

  • A mutation (POST/PATCH/PUT/DELETE) on a table invalidates that table’s entries.
  • A volatile RPC call (a function that may modify data) invalidates every table’s entries.
  • A schema cache reload invalidates everything, so no response cached against the old schema is served.

Only the directly-written table is invalidated by a mutation: a write that cascades to other tables through foreign keys or triggers leaves their entries in place until they expire. The ttl_seconds window is the upper bound on staleness for such cases (and for 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 workloads. Frequent writes to a table keep invalidating its entries before they pay off, and cascading writes (FKs, triggers) can leave other tables’ entries stale for up to ttl_seconds. The cache targets read-heavy workloads.

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

{
"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 },
"pool": { "max_size": 16, "size": 4, "available": 3, "waiting": 0 }
}

The endpoint is always available; stats is null when caching is disabled, and pool is null on SQLite. It authenticates like the data API (a 401 without a token when jwt_secret is set and there is no anon_role).

FieldTypeDefaultDescription
pre_requeststring?unsetFully-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?unsetTitle in the OpenAPI document’s info section.
openapi_server_urlstring?unsetServer 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.

There are no TLS config keys: TLS follows the DSN’s sslmode parameter, for the primary, every replica and the pub/sub listener.

sslmodeBehaviour
disablePlaintext only
prefer (default)TLS whenever the server offers it. If the server’s certificate does not verify, the connection fails — it does not fall back to plaintext
requireTLS is mandatory, and the certificate is verified

Server certificates are always verified against the platform trust store; set SSL_CERT_FILE to point at a CA bundle (for example your provider’s root CA) when the server’s CA is not in it. libpq-only parameters such as sslrootcert, sslcert, and the verify-ca/verify-full modes are not supported.

Terminal window
pgvis --dsn "postgres://app:secret@db.example.com:5432/app?sslmode=require" serve
SSL_CERT_FILE=/etc/ssl/certs/my-db-ca.pem pgvis --dsn "postgres://…?sslmode=require" serve

The DSN’s password is redacted (***) in pgvis’s startup logs. PostgreSQL 17 or newer is required (17 and 18 are tested); older servers are rejected at startup.

There are no config keys for reloading. pgvis re-introspects the schema, debounced, after any of:

  • NOTIFY pgrst, 'reload schema' (or an empty payload) from any session on Postgres — e.g. at the end of a migration. pgvis always listens on the pgrst channel (PostgREST’s convention); with replicas, it listens on the primary
  • SIGUSR1 to the pgvis serve process

A failed re-introspection keeps the current schema cache. A reload also invalidates the data cache.


[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
-b, --bind <ADDR>PGVIS_BINDBind address (default: 127.0.0.1:3000, loopback only; pass e.g. 0.0.0.0:3000 to expose it)
--insecure-no-authPGVIS_INSECURE_NO_AUTHAllow starting with neither jwt_secret nor anon_role; every request then runs as the DSN’s own role. Without it, serve refuses to start in that case. Local development only
-s, --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.ttl_seconds)
--cache-max-entries <N>PGVIS_CACHE_MAX_ENTRIESMax cache entries (cache.max_entries)
--cache-listsPGVIS_CACHE_LISTSAlso cache list queries (not just PK lookups)
--pubsub-enabledPGVIS_PUBSUB_ENABLEDEnable pub/sub (SSE at /pubsub/{channel} and MCP tools)
--pubsub-channel-prefix <PREFIX>PGVIS_PUBSUB_CHANNEL_PREFIXPub/sub channel prefix (pubsub.channel_prefix)
--mcp-http—Also serve MCP at /mcp via Streamable HTTP

serve is the default command, but when the subcommand is omitted its flags take their built-in defaults and the serve-only variables above (PGVIS_BIND, PGVIS_INSECURE_NO_AUTH, PGVIS_CACHE_*, …) are not read. Name serve explicitly.

FlagEnvDescription
-s, --schema <NAME>PGVIS_SCHEMASSchemas to expose
--read-only—Expose only read tools (no create/update/delete/RPC); same as read_only = true

No flags. openapi prints the OpenAPI 3.0 document; inspect dumps the introspected schema cache as JSON. Both write to stdout and exit.

Global options go before the subcommand (pgvis --dsn … serve, not pgvis serve --dsn …).

FlagEnvDescription
-d, --dsn <DSN>PGVIS_DSNDatabase connection string (required)
-c, --config <FILE>PGVIS_CONFIGPath to TOML config file
SignalEffect (pgvis serve)
SIGUSR1Reload the schema cache
SIGTERM / Ctrl-CGraceful shutdown: stop accepting connections and finish in-flight requests

When the same setting is specified in multiple places, the highest-priority source wins: CLI flags, then environment variables, then the TOML file, then the 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. When false no listener connection or background task is created
pubsub.channel_prefixstring"pgvis:"Prefix added to Postgres LISTEN/NOTIFY channel names
pubsub.max_payload_bytesinteger7500Maximum message payload size in bytes, checked before publishing (Postgres caps NOTIFY payloads at ~8000 bytes)
pubsub.max_subscribersinteger1000Maximum concurrent subscribers across all channels; beyond it new subscriptions get a 503
pubsub.max_subscribers_per_identityinteger100Concurrent SSE subscribers per JWT role + sub (0 = no limit); callers without sub (e.g. anonymous clients) can’t be told apart and are bounded by max_subscribers only
pubsub.channel_buffer_sizeinteger64Per-channel broadcast buffer; a subscriber that falls further behind receives a “lagged” notice
pubsub.allowed_channelsstring[][]Channel allowlist (empty = allow all). Supports globs: chat.*
pubsub.authorize_functionstringunsetSQL function called as fn(channel text, op text) (op is 'subscribe' or 'publish') in a transaction running as the caller’s role with their JWT claims set. Anything but true (false, NULL, or an error) is a 403. Unset = any caller that passes JWT verification may use every channel in allowed_channels
pubsub.reconnect_base_msinteger500Listener reconnect backoff base (base * 2^attempt)
pubsub.reconnect_max_msinteger30000Listener reconnect backoff cap
pubsub.keepalive_interval_secsinteger15Interval of the SSE : keepalive comment that keeps idle connections open through proxies

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 (serve only)
PGVIS_PUBSUB_CHANNEL_PREFIXpubsub.channel_prefix (serve only)
PGVIS_PUBSUB__<KEY>any scalar pubsub.<key>, e.g. PGVIS_PUBSUB__MAX_SUBSCRIBERS=5000

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-audjwt_aud
jwt-role-claim-keyrole_claim_key (top-level claim name only)
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