Skip to main content
Published data models are the recommended way to give agents data. When code needs to query a database directly — ad-hoc SQL, a table that is not modeled yet, a long-running app — connect from the sandbox with a read-only role. This guide covers the role, the 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.
You will need:
  • 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 only SELECT — never write, drop, or alter. This bounds the blast radius: a leaked credential exposes data at worst, it can never mutate or destroy anything.
Run this as a superuser or the database owner, connected to <DB>:
create_readonly_role.sql
The last line is optional. GRANT SELECT ON ALL TABLES only covers tables that exist today; ALTER DEFAULT PRIVILEGES extends read access to tables created later. Run the script as the role that owns the tables.
Run the script:
Verify it is actually read-only:

2. Store the connection as PG* secrets

Store the connection as separate libpq PG* 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.
The host is the value that changes with where the code runs:
For Neon, the connection details are under Console -> your project -> Connection Details. Use the pooled (-pooler) host for agent and sandbox workloads.
Reachability. A private address like 10.10.0.15 only resolves from a sandbox on the same network. On-prem DataGen runs the sandbox inside your network, so it reaches the database directly — no tunnel. From a cloud sandbox, the database must be reachable via firewall allowlist, VPC peering, or a tunnel.
Prefer one value? Add a single Secret with Target DATABASE_URL and the full URL as its value, assembled from the same pieces:
Managed databases add TLS parameters, for example Neon:
Read it with os.environ["DATABASE_URL"]. Set Type to Per-user when the URL embeds a personal credential, otherwise Shared with workspace.
If the password contains URL-special characters (@ : / ? # %), URL-encode them — @ becomes %40. The PG* approach avoids this entirely because libpq takes each value literally.

3. Bake the query packages

Give the sandbox pandas, 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

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.
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.
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.
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.
Quote PascalCase table names in raw SQL: 'SELECT * FROM "Workspace"'. Unquoted, Postgres lowercases the identifier and reports the table does not exist.
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* or DATABASE_URL), never in code or git.
  • ALTER DEFAULT PRIVILEGES is in place so future tables stay readable.
  • (Postgres 14 and older) CREATE is revoked from PUBLIC so the role cannot write.
  • Cloud databases use sslmode=require (add channel_binding=require where 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.