::castinjection — cast targets inselectare validated at parse time and rejected with 400 otherwise; a cast can't be a bind parameter, so it was interpolated verbatim and could alter the SELECT (reachable by any role that can read).- DDL quoting — quote the remaining
CREATE/ALTER DATABASEidentifiers, and whitelist grant/revoke verbs and object types instead of interpolating them.
- Shutdown now waits for in-flight requests to complete; the wait was previously hardcoded to 1 second, so every restart killed any request slower than that. The new
GracefulShutdownTimeoutconfig key (seconds, default 0 = wait until done) bounds the wait for deployments that want a hard cap below their supervisor's stop grace period. A second signal during the wait forces an immediate exit, andShutdown()is now idempotent. /readynow reports503 {"status":"draining"}as soon as a graceful shutdown begins, while/livekeeps answering 200 until the process exits — the standard probe contract for zero-downtime rolling deploys. The newDrainDelayconfig key (seconds, default 0 = disabled) keeps the listener serving for that long after readiness flips, giving load balancers time to deregister the instance before it stops accepting connections. The delay applies to SIGTERM only; an interactive Ctrl-C (SIGINT) shuts down immediately, and a second signal during the window skips it.
- Schema cache held in an atomic pointer (was a data race on reload).
- Serializers return an error on a malformed wire buffer or a type/dimension mismatch instead of panicking or silently misparsing.
limit/offsetreject non-integer or negative values (was a silentLIMIT 0).*→%wildcard rewrite scoped tolike/ilike(was applied to every value).- Boolean-filter nesting capped at 100 levels (stack-overflow DoS).
- Invalid grant input returns 400 instead of 500.
- Session watcher no longer deadlocks when paused.
- SIGTERM — the server now shuts down gracefully on SIGTERM (the default stop signal of Docker, Kubernetes and systemd), not only on SIGINT/Ctrl-C. A raw SIGTERM previously terminated the process immediately, cutting in-flight requests and resetting database connections.
- Exit status — a graceful shutdown now exits with status 0; it previously exited 1, which supervisors interpret as a crash.
- Database connections on shutdown — connection pools and the notification listener's dedicated connection are now closed after the HTTP server drains, so Postgres sees clean disconnects instead of connection resets on every stop.
NotificationListener.Stop()now interrupts a pendingWaitForNotificationand waits for the listener goroutine to exit before returning.
- SQL injection — closed four sinks where request tokens were interpolated into SQL with bare quotes instead of being escaped/parameterized (a backslash escape in the parser let a client smuggle a literal
'/"): theselectalias, embedded-resource filter values, full-text-search config arguments, and JSON-path members. Top-level filter values and identifiers were already parameterized and unaffected. SET ROLE— the JWTroleclaim is now quoted as an identifier so it cannot break out of the statement.- Empty JWT secret — the server now refuses to start when authentication is enabled and
JWTSecretis empty (an empty HMAC key lets anyone forge tokens); previously only a warning. Debug mode auto-generates a random secret. - TLS fail-closed — a configured certificate that fails to load is now a fatal startup error instead of silently serving plaintext HTTP.
- Config file permissions — written
0600(was0777), since it holds the JWT secret and the database password.
- Generic jq support via gojq, disabled by default (
JQ.Enabled). Evaluation is bounded: no I/O, per-evaluation timeout (JQ.Timeout), program size cap (JQ.MaxProgramBytes), compiled-program LRU cache (JQ.CacheEntries), and an exactly-one-output convention. Three surfaces over one shared core (newjqevalpackage):- jq updates:
PATCH /table?filters&jq=<program>performs an atomic read-modify-write of the matched rows — rows are selectedFOR UPDATEinside the request transaction, each row is fed to the program, and its object output becomes that row'sUPDATE ... SET. All-or-nothing, capped byJQ.MaxUpdateRows; RLS and triggers apply unchanged. The program can also be sent as the raw request body withContent-Type: application/vnd.smoothdb.jq - Response transforms:
jq=on table reads and RPC calls transforms the JSON response body;Content-Range/count headers reflect the pre-transform result set POST /jq: standalone batch evaluation endpoint with per-item errors andparse_onlycompile-checking for authoring-time validationjq_args=(URL-encoded JSON object) binds values as jq variables on both updates and transforms
- jq updates:
recurse!upoperator for single-table recursion — walks the FK toward ancestors instead of descendants (e.g.parent_id=recurse!up.allreturns a node's ancestor chain). Rejected withvia(), where the same reversal is done by swapping the edge columns.
- Ordering a
via()recursive query by__depthwithout selecting it no longer 500s with "ORDER BY expressions must appear in select list";__depthis now surfaced in the projection and routed through the min-depth dedup wrapper.
- Computed-relationship embeds (a function of the parent row, e.g.
labels_objects(document)) now compose with single-table recursion — they previously failed withcolumn "<table>" does not existbecause the function's row argument wasn't repointed to the recursive CTE. Plain FK and spread embeds were unaffected.
__depthselectable pseudo-column on recursive queries — reports each row's traversal depth (seed = 0); inviamode it reports the shallowest depth at which a node is reachedwalk.filter prefix to prune the traversal — skips a non-matching node and everything beyond it- Bidirectional graph traversal with
via!both(src,dst)— follows edges in either direction - Resource embedding now composes with single-table recursion (not supported together with
via()traversal)
- Breaking: plain filters on recursive queries now filter the result instead of pruning the traversal; the previous prune-the-walk behavior is now opt-in via the
walk.prefix
- JSONB filter behavior now matches PostgREST (#19): JSON-path filters with quoted string values, containment against JSON string arrays, and whole-column JSONB equality/inequality
- Improved release process (goreleaser config and
make releasetarget) - Updated Go and UI dependencies
- Configurable JWT token expiry (
TokenExpiryconfig, default 24h) - Configurable error verbosity (
VerboseErrorsconfig, default true) to control whether database hints/details are returned to clients - Security headers middleware (
X-Content-Type-Options,X-Frame-Options,Strict-Transport-Security) - Config startup validations: warn on empty JWT secret, reject CORS credentials with wildcard origins, warn on unlimited request body size
- Fixed parallel test suites port collision (postgrest suite moved to port 8084)
- SQL injection in
GetPolicies,GetDatabasePrivileges, andGetPrivileges— switched from string concatenation to parameterized queries - JWT algorithm validation now pinned to HS256 (was accepting any HMAC variant)
- CORS default changed from wildcard
*to empty (must be explicitly configured) - CORS allowed headers restricted to specific list instead of wildcard
- Admin
/sessionsendpoint now requires authentication - TLS minimum version enforced to TLS 1.2
- Session keys now use SHA-256 hash instead of raw token strings
- Removed hardcoded JWT secret from sample config
- RPC calls now emit named parameters in a deterministic order (function-signature order, falling back to alphabetical), so
pg_stat_statementsaggregates identical calls under a singlequeryidinstead of one per JSON key permutation - M2M view relationship synthesis now deterministically prefers view junctions over base-table junctions (fixes a flaky "permission denied" error when the base junction lived in a schema the caller lacked USAGE on)
- Session improvements and tests
- Support for recursive queries
- Support for any/all operator modifiers on eq, like, ilike, gt, gte, lt, lte, match, imatch (e.g.
last_name=like(any).{O*,P*}) - Support for
is.not_nullfilter value (andnot.is.not_null, case-insensitive) - Support for
isdistinctoperator (IS DISTINCT FROM) - Support for full-text search on text and jsonb columns (auto
to_tsvector()wrapping) - Resource embedding with views and materialized views (including chained views, aliased columns, M2M through view junctions)
- Support for setting and retrieving comments on tables and columns via the DDL Admin API
- Fixed incorrect native type casting for boolean and null values when filtering JSONB via
->operator - Fixed table creation in non-default schema returning 404
- Fixed owner change ordering in table creation and update (owner is now set last to avoid permission errors)
- Support for Computed Relationships (table-returning functions as virtual relationships)
- Support filtering embedded resources by FK column name (e.g.
client_id.id=eq.1) - Related orders: order parent rows by columns in to-one embedded resources (e.g.
order=clients(name).desc)
- Fixed serialization bugs for composite and unrecognized types
- Fixed CI race condition
- Schema Cache Reload via PostgreSQL NOTIFY
- Route for listing columns in /api (eg /api/db/$info/table)
- Server version in the HTTP header
- CORS environment variables
- Health endpoints
- Route for listing tables in /api
- Disambigue embedded resources by fk contraints; hints by fk and related cols; !left; tests
- Support comma-separated Prefer values
- Fix for 32bit arch
- Dependencies
- Login route
- Env variable for JWT secret
- Support for Char and UUID types
- Flexible JWT claims
- Support quotes on columns and on_conflict fields
- Fixes on array serialization
- Fields in JSON struct for errors in lowercase
- Possible loop in the parser with invalid select list
- Small refactoring for UI code
- Dependencies updates
- Better docs for installing and some other minor fixes
- Using goreleaser to publish binaries and support homebrew, still experimental
- Admin UI, first steps
- Plugins, first experimental release
- Create db with owner
- Roles and Databases update
- count=exact and tests
- Separate pretty and color log configuration
- Explicit schema
- Schema for DDL operations as header
- Some fixes to logging
- Support for application/octet-stream in input and output
- Support for HEAD calls
- Configuration for ReadTimeout, WriteTimeout and RequestMaxBytes
- Possible panic on logging
- CSV as input and output
- Support for application/x-www-form-urlencoded input data
- Support for Date Postgres type
- Fix table columns quoting
- Fix json keys quoting (allow names with ")
- Fix to properly order function args when getting function info
- Support for Range header
- Functions with GET
- Support for domain types for functions
- Better quoting to prevent SQL injection
- Concept of main database, no more special dbe pool
- Accept empty IN clauses
- Logging.FileLogging config was not effective