connection per request → pooling
Connection pooling
A single pgx/pgxpool fronts Postgres instead of opening a fresh connection on every click — no connection storms.
pgpeek is a minimal, read-only, team-shared Postgres browser — a sidebar of tables, paged row browsing, a SQL scratchpad with saved queries, and CSV export everywhere. Built to replace Adminer, shipped as one ~25 MB distroless binary.
Press Ctrl/⌘ + Enter to run a query.
Every failure mode that made the old tool wedge a pod was designed out on purpose.
connection per request → pooling
A single pgx/pgxpool fronts Postgres instead of opening a fresh connection on every click — no connection storms.
OOM on huge results → row cap
Results stop at PGPEEK_ROW_CAP; an enormous table is never fully buffered into memory, and the UI tells you when output was capped.
runaway query → statement timeout
statement_timeout is set on every pooled session, so a slow query can't wedge the pod for everyone else.
per-pod state → stateless
Saved queries live in a tiny SQLite file on a PVC. The query path holds no per-user state, so pods are disposable.
No row editing, no schema management, no migrations — browsing and querying, done well.
=, ≠, <, >, ≤, ≥, ILIKE, LIKE, IS NULL, NOT NULL.
= value filter automatically.
SELECT / WITH only — enforced.Twenty built-in color themes, switchable from the header dropdown — pgpeek remembers your choice in the browser. No rebuild, no config.
Read-only isn't a setting you can forget to flip — it's enforced at the role, the session, and the app.
pgpeek connects with a role that has no write privileges. This is what actually keeps your data safe — everything else is belt-and-suspenders.
pgpeek sets default_transaction_read_only = on on every pooled connection, so even an accidental write is rejected by Postgres.
internal/guardRejects anything that isn't a single SELECT / WITH / VALUES / TABLE / EXPLAIN — blocking multiple statements and DML/DDL, and aware of keywords hiding inside comments and string literals.
pgx.Identifier; operators come from a fixed allowlist; values are bound as query parameters; sort is ASC/DESC only. No user input is ever concatenated into SQL.
The browsed database stays read-only; a tiny independent SQLite file holds saved queries.
jackc/pgx/v5 · modernc.org/sqlite (pure-Go, no cgo)
go:embed · CSP-safe
Run pgpeek locally with Docker Compose.
Create a pgpeek-only compose file and start it.
cat > compose.yml <<'YAML'
services:
pgpeek:
image: ghcr.io/descope-sample-apps/pgpeek:latest
environment:
DATABASE_URL: ${DATABASE_URL}
ports: ["8080:8080"]
YAML
export DATABASE_URL='postgres://descoperead:PASSWORD@host:5432/db?sslmode=require'
docker compose up
Open http://localhost:8080. Stop with docker compose down.
Point pgpeek at any reachable Postgres with a read-only role.
export DATABASE_URL='postgres://descoperead:PASSWORD@host:5432/db?sslmode=require'
export PGPEEK_STORE_PATH=./pgpeek.db
go run .
Produce a static binary or a distroless image.
make build # static binary (CGO disabled)
make image # distroless image via goreleaser + ko
docker build -t pgpeek . # or the hand-written Dockerfile
Any value can also come from a mounted file via <VAR>_FILE — Docker / Kubernetes secrets friendly.
| Variable | Default | Notes |
|---|---|---|
| DATABASE_URL | required | Postgres DSN. Use the read-only role. Never logged. (DATABASE_URL_FILE reads it from a mounted secret.) |
| PGPEEK_LISTEN | :8080 | Listen address. |
| PGPEEK_ROW_CAP | 1000 | Max rows returned/exported per query. |
| PGPEEK_STATEMENT_TIMEOUT | 30s | Per-query DB statement timeout. |
| PGPEEK_IDLE_TX_TIMEOUT | 30s | idle_in_transaction_session_timeout. |
| PGPEEK_MAX_CONNS | 8 | Max pool size (caps DB connection usage). |
| PGPEEK_STORE_PATH | /data/pgpeek.db | SQLite file for saved queries. |
| PGPEEK_READ_HEADER_TIMEOUT | 10s | HTTP read-header timeout. |
| PGPEEK_WRITE_TIMEOUT | stmt+30s | HTTP write timeout (must exceed statement timeout for big exports). |
| PGPEEK_IDLE_TIMEOUT | 120s | HTTP keep-alive idle timeout. |
| PGPEEK_SHUTDOWN_TIMEOUT | 15s | Graceful-shutdown grace period. |
| PGPEEK_TLS_CERT_FILE | — | Enable HTTPS (set with the key file). Otherwise terminate TLS at the ingress. |
| PGPEEK_TLS_KEY_FILE | — | TLS private key path. |
| PGPEEK_DB_IAM_AUTH | false | Use RDS/Aurora IAM auth instead of a password. |
| PGPEEK_AWS_REGION | $AWS_REGION | AWS region for IAM token signing (required when IAM auth is on). |
PGPEEK_DB_IAM_AUTH=true and a region, drop the password from the DSN, and pgpeek mints a short-lived IAM token from the default AWS credential chain (env / web-identity / IRSA / instance role) before every new connection — no static DB password stored anywhere.
The UI is just a client of these endpoints — script against them directly if you like.
| Method & path | Purpose |
|---|---|
| POST /api/query | Run a query → JSON {columns, rows, …}. |
| POST /api/export | Run a query → CSV download. |
| GET /api/meta | Server limits the UI needs ({rowCap}). |
| GET /api/tables | List browsable tables/views (+ row estimate). |
| GET /api/tables/{schema}/{table}/columns | Column structure. |
| GET /api/tables/{schema}/{table}/fks | Single-column foreign keys (for click-through). |
| GET /api/tables/{schema}/{table}/data | Paged rows: ?limit=&offset=&search=&sort=&dir=&f=col:op:val (&format=csv). |
| GET /api/queries | List saved/preset queries. |
| POST /api/queries | Create a saved query. |
| PUT /api/queries/{id} | Update a saved query. |
| DEL /api/queries/{id} | Delete a saved query. |
| GET /healthz · /readyz | Liveness · readiness (pings the DB). |
| GET / | The UI. |
Manifests live in k8s/ — Deployment, Service, PVC, optional Ingress, and a ServiceAccount.
Runs nonroot with a read-only root filesystem (only /data writable), drops all capabilities, and ships liveness /healthz + readiness /readyz probes.
The saved-query store is a SQLite file on a ReadWriteOnce PVC, so the Deployment ships replicas: 1 + Recreate. Move it to a shared backend to scale out.
release-please + goreleaser + ko publish multi-arch distroless images with SBOMs to ghcr.io/descope-sample-apps/pgpeek.
Unit + integration tests run with the race detector against a real Postgres service in CI.
golangci-lint (errcheck, gosec, revive, …) and govulncheck gate every change.
Tagged via release-please; built by goreleaser + ko into signed, multi-arch images with SBOMs.