PostgreSQL access governance — every statement reviewed before it runs
Last updated
AccessFlow governs PostgreSQL as a full query proxy: your team connects to AccessFlow, not to Postgres, and every statement is parsed to an abstract syntax tree, checked against a schema allow-list, risk-scored, routed to an approver, and only then executed — under column masking, row-level security and a row cap. PostgreSQL is the one engine that needs no install step: it is bundled, and it is also the database AccessFlow itself runs on.
- Family
- Relational
- Query language
- SQL
- Runs as
- In-process pooled JDBC
- Default port
5432- Default SSL mode
VERIFY_FULL- Install
- Bundled — nothing to download
How AccessFlow connects
PostgreSQL runs on the in-process JDBC lane — a pooled HikariCP datasource inside the AccessFlow process, not a downloaded engine plugin. The driver ships with the application, so the connector is marked bundled in the catalog and a fresh install can govern a Postgres datasource with no network fetch at all.
The datasource takes the usual host, port (5432 by default), database name, username and password. The password is AES-256-GCM encrypted at rest and decrypted exactly once, at pool initialisation — it is never held in the heap beyond that, and never serialized into any API response. It can also be stored as a reference to HashiCorp Vault, AWS Secrets Manager or Azure Key Vault instead of in the AccessFlow database at all.
PostgreSQL is the only connector whose default SSL mode is VERIFY_FULL — full certificate and hostname verification, rather than the REQUIRE most connectors default to. A datasource with a read replica configured routes SELECTs to the replica and writes to the primary.
What AccessFlow understands
Every submitted statement is parsed by JSqlParser into a real syntax tree before anything else happens. That tree is what the allow-list is checked against — AccessFlow walks the AST to collect referenced tables rather than matching strings, so a table name hidden in a comment, a CTE alias or a quoted literal cannot smuggle access past the check.
The parsed statement is classified onto a query type — SELECT, INSERT, UPDATE, DELETE or DDL — and that classification drives the per-user read, write and DDL capabilities, the routing-policy engine and the approval workflow. Unparseable SQL is rejected with HTTP 422 before it reaches a connection.
What it refuses
Multi-statement input is rejected outright, with one deliberate exception: a BEGIN; … COMMIT; envelope wrapping a homogeneous batch of INSERT, UPDATE or DELETE statements, which runs under a single JDBC transaction and rolls back as a unit on failure. Inside that envelope, SELECT, DDL, ROLLBACK, SAVEPOINT and a nested BEGIN are each rejected with their own message.
Nothing the proxy executes is ever built by string concatenation. Every value — including the ones row-level security itself injects — is bound through a prepared statement, so a policy value can never alter the shape of the statement it is filtering.
Row-level security and masking
A row-security policy becomes an extra predicate spliced into the statement's WHERE clause, with its values bound as positional parameters. Shapes the rewriter cannot provably narrow — rather than silently widen — fail closed with HTTP 422 instead of executing unfiltered.
Column masking is applied to the result after fetch, so a masked column is never returned in the clear even if the query selected it explicitly. Applied policy ids are recorded on the query alongside the result, so a reviewer reading the audit log later can see exactly which policies shaped what the user saw.
What the rewrite actually does
Here is the whole mechanism in one example. A policy restricting the orders table to the caller's own region turns a plain read into a narrowed one before it reaches Postgres:
-- submitted SELECT id, email FROM orders WHERE status = 'OPEN'; -- executed (policy: orders.region = the caller's region) SELECT id, email FROM orders WHERE (status = 'OPEN') AND orders.region = ?; -- ^ bound, never concatenated
The policy value rides as a bound parameter, so it cannot change the shape of the statement it is filtering. A join, a set operation or a subquery the rewriter could not provably narrow would have been refused with HTTP 422 instead of executed with the filter quietly dropped.
Introspection, dry-run and cost estimates
Schema introspection reads the catalog into the same schema view that feeds the ER diagram, the editor's autocomplete and the AI analyzer's prompt context. Sensitive columns can be tagged by hand or proposed automatically by the discovery scanner.
A dry-run plans the governed statement with EXPLAIN (FORMAT JSON) — the row-security predicate already spliced in — and maps the result into a plan tree, without executing anything. The same machinery runs automatically on submission to produce a blast-radius estimate, and for an UPDATE or DELETE it also computes the exact affected-row count by rewriting the statement into a SELECT COUNT(*) that goes through row security like any other read.