PG* secrets that carry the
connection, the packages, and the query code.
It builds on two foundation pages:
- Vault — how secrets become environment variables, and Shared vs Per-user.
- Sandbox Environments — how packages are baked into an image.
Prerequisites
This guide is specific to PostgreSQL (local, Neon, RDS, or any
Postgres-compatible database). The read-only role SQL, the
psycopg2 driver,
and the connection URL format are all Postgres-specific — other engines
(MySQL, SQL Server, BigQuery, etc.) use different roles, drivers, and syntax.- A PostgreSQL database and a superuser or owner login to create the role.
- Access to run SQL (via
psql,pgAdmin, or your provider’s SQL editor). - An agent or app in DataGen whose code will run the queries.
Managed databases (Neon, RDS, Supabase) are already reachable over TLS, so no
tunnel is needed — any sandbox with outbound network connects directly. Tunnels
are only relevant for exposing a database that lives on your own machine.
1. Create a read-only user
Agents connect with a role that can onlySELECT — never write, drop, or alter.
This bounds the blast radius: a leaked credential exposes data at worst, it can
never mutate or destroy anything.
- SQL (any Postgres)
- pgAdmin
Run this as a superuser or the database owner, connected to Run the script:
<DB>:create_readonly_role.sql
2. Store the connection as PG* secrets
Store the connection as separate libpqPG* variables. Postgres drivers read
these automatically, so your code never assembles or parses a URL, and passwords
with special characters need no encoding.
In the vault bound to your agent or app, add each row below
with Add credential -> Secret, setting Target to the variable name and
Type by scope:
Host, port, database and sslmode are the same for everyone, so an admin sets
them once as Shared. User and password belong to each member, so they are
Per-user: each member clicks Connect on the row and enters their own.
Use the libpq names exactly. DataGen uppercases the Target, so
pghost also
arrives as PGHOST, but a different name such as PSQL_PASSWORD is never read
by libpq.Alternative: a single DATABASE_URL secret
Alternative: a single DATABASE_URL secret
Prefer one value? Add a single Secret with Target Managed databases add TLS parameters, for example Neon:Read it with
DATABASE_URL and the
full URL as its value, assembled from the same pieces:os.environ["DATABASE_URL"]. Set Type to Per-user when
the URL embeds a personal credential, otherwise Shared with workspace.3. Bake the query packages
Give the sandboxpandas, sqlalchemy and psycopg2-binary through a
Sandbox Environment. The built-in Data
Engineering template already includes them (plus duckdb, polars and
pyarrow). Create an environment from it, wait for the ready badge, then
select it on your agent or app.
Alpine / musl base:
psycopg2-binary ships only glibc wheels. On a musl
image, use psycopg[binary] (psycopg 3 ships musl wheels) and connect with a
postgresql+psycopg:// URL instead of postgresql+psycopg2://.4. Query it
Quick end-to-end check
Troubleshooting
Run fails with "Vault not ready"
Run fails with "Vault not ready"
A member the run runs as has not filled in their Per-user
PGUSER /
PGPASSWORD. They click Connect on each row in the vault. See
Vault gotchas.ModuleNotFoundError: sqlalchemy / psycopg2
ModuleNotFoundError: sqlalchemy / psycopg2
The agent or app is not bound to an environment with the query packages, or
the environment is still building. Check the badge on
Sandbox Environments.
Why pool_pre_ping=True
Why pool_pre_ping=True
A pooled connection can go stale between queries — the server’s idle timeout
drops it, the DB restarts, or a serverless backend (Neon) suspends.
pool_pre_ping sends a cheap SELECT 1 before reusing a connection, turning a
stale-connection crash into a transparent reconnect. Cheap insurance for any
long-running or intermittently-idle sandbox.First query is slow (1-3 seconds)
First query is slow (1-3 seconds)
The first connection after a Neon database has been idle triggers a cold start
while the compute wakes up. Set a generous
connect_timeout — it is not a
network fault, and the next query will be fast.Table "does not exist" errors
Table "does not exist" errors
Quote PascalCase table names in raw SQL:
'SELECT * FROM "Workspace"'.
Unquoted, Postgres lowercases the identifier and reports the table does not
exist.Connection times out
Connection times out
The sandbox cannot reach the host. A private IP only works from a sandbox on
the same network; from a cloud sandbox, allowlist the database or use a
tunnel or peering.
Security checklist
- The sandbox connects with the read-only role, never the owner.
- The connection lives in vault secrets (
PG*orDATABASE_URL), never in code or git. ALTER DEFAULT PRIVILEGESis in place so future tables stay readable.- (Postgres 14 and older)
CREATEis revoked fromPUBLICso the role cannot write. - Cloud databases use
sslmode=require(addchannel_binding=requirewhere supported). - Rotate the password if it was ever pasted into a chat, log, or ticket.
Next
Files
Upload documents and spreadsheets your agents can read.
Use data in agents
Attach published data models to agents and apps instead of raw SQL.