Access a Private PostgreSQL Database Without a VPN using rstream
Keep PostgreSQL inside a private network while developers, CI jobs, and migration tools connect through a private rstream bytestream tunnel.
Private PostgreSQL access comes up in everyday work. A developer inspects staging data, CI runs migrations, support needs readonly access, or a small team keeps a database in an office, lab, or private cloud network. The right setup keeps the database off the public Internet while existing tools keep using a normal PostgreSQL host and port.
With rstream private tunnels, PostgreSQL keeps listening where it already lives. The machine that can reach the database opens an outbound rstream session, and a developer laptop, admin workstation, or CI runner uses rstream nc to create a local TCP port that standard database clients can use.
The sample below uses a real PostgreSQL container and a private rstream bytestream tunnel. psql, Prisma, Drizzle, node-postgres, migration tools, and CI jobs all connect through the same local PostgreSQL URL on 127.0.0.1.
The companion sample contains the database container, schema initialization, tunnel commands, and verification target.
rstream examples / private-postgres-accessOpen the private PostgreSQL access sample.git clone https://github.com/rstreamlabs/rstream-examples.git
cd rstream-examples/private-postgres-accessWhat You Will Build
The private network side owns PostgreSQL and the publishing side of the tunnel. The client side opens a local port and connects normal database tools to it.
psql, Prisma, Drizzle, migration job
|
| PostgreSQL URL on 127.0.0.1:15432
v
local rstream nc adapter
|
| private rstream bytestream
v
rstream engine
|
| outbound session from the private network
v
PostgreSQL on 127.0.0.1:55432 or a private LAN addressIn this setup, clients authenticate to rstream and need permission to dial the private tunnel. PostgreSQL still authenticates the database user after the network path is established.
The server-side command publishes a private rstream endpoint named staging-postgres and forwards accepted streams to PostgreSQL. The client-side command opens 127.0.0.1:15432 on the developer laptop or CI runner and dials that private endpoint.
rstream nc is the TCP adapter on both sides. -L declares where the command listens, and -R declares where each accepted connection is sent. When an endpoint uses rstrm://<name>, that side of the adapter is a private rstream tunnel instead of a local TCP socket.
# Server side, on the machine that can reach PostgreSQL.
rstream nc -L rstrm://staging-postgres -R 127.0.0.1:55432
# Client side, on the developer laptop or CI runner.
rstream nc -L 127.0.0.1:15432 -R rstrm://staging-postgresClients then connect to the local PostgreSQL URL.
postgres://app:app@127.0.0.1:15432/appPrerequisites
Before starting, make sure you have:
- Docker with Compose for the sample database.
- The rstream CLI on the database-side host and on the client-side machine.
- A selected rstream project or a self-hosted engine context.
On hosted rstream, private tunnels require a project plan that supports private tunnel creation and private tunnel dialing. Self-hosted CE supports private bytestream tunnels for direct engine deployments. Keep database credentials separate from rstream credentials in both cases.
Log in and select the project on both sides.
rstream login
rstream project use <project-endpoint> --defaultFor CI, use a project-scoped token or application credential with the smallest tunnel permissions needed for the job.
Start PostgreSQL
Start the sample PostgreSQL service and run the verification query.
make verifyThe verification starts postgres:16-alpine, waits until PostgreSQL is ready, and confirms that the initialized notes table is reachable through PostgreSQL.
select id, body from notes order by id limit 5;The local database listens on this address.
127.0.0.1:55432The sample database contains a small notes table initialized from db/init/001_schema.sql.
Create the private tunnel
On the machine that can reach PostgreSQL, run the server-side tunnel.
TUNNEL_NAME=staging-postgres make server-tunnelThe Makefile expands that target to the rstream nc command below.
rstream nc -L rstrm://staging-postgres -R 127.0.0.1:55432This command creates a private bytestream tunnel named staging-postgres and forwards each accepted rstream stream to PostgreSQL. Keep this process running next to the database, or run the equivalent command under your service manager.
For a real deployment, replace 127.0.0.1:55432 with the address that is correct from the database-side host.
rstream nc -L rstrm://staging-postgres -R 10.10.20.15:5432Only the host running the server-side rstream client needs to reach that private address.
Open a local client port
On the developer laptop, admin workstation, or CI runner, open the local client port.
TUNNEL_NAME=staging-postgres CLIENT_PORT=15432 make client-portThe Makefile expands that target to the client-side rstream nc command below.
rstream nc -L 127.0.0.1:15432 -R rstrm://staging-postgresThe client now has a local TCP adapter on 127.0.0.1:15432 that normal PostgreSQL tools can use.
With a local psql client, use this command.
psql "postgres://app:app@127.0.0.1:15432/app" \
-c 'select id, body from notes order by id limit 5;'With Docker Desktop on macOS or Windows, use host.docker.internal.
docker run --rm postgres:16-alpine \
psql "postgres://app:app@host.docker.internal:15432/app" \
-c 'select id, body from notes order by id limit 5;'In both cases, the database client only needs a PostgreSQL URL.
Use the same URL from application tools
Application tooling can use the local adapter as its database host.
export DATABASE_URL="postgres://app:app@127.0.0.1:15432/app"Run Prisma migrations with the usual command.
npx prisma migrate deployRun Drizzle migrations the same way.
npx drizzle-kit migrateNode scripts can reuse the same DATABASE_URL.
node scripts/run-maintenance-job.mjsIn CI, start the local adapter before running migrations and wait until PostgreSQL responds.
rstream nc -L 127.0.0.1:15432 -R rstrm://staging-postgres &
RSTREAM_NC_PID=$!
trap 'kill "$RSTREAM_NC_PID" 2>/dev/null || true' EXIT
for attempt in $(seq 1 30); do
if pg_isready -h 127.0.0.1 -p 15432 -U app -d app >/dev/null 2>&1; then
break
fi
sleep 1
done
pg_isready -h 127.0.0.1 -p 15432 -U app -d app
export DATABASE_URL="postgres://app:app@127.0.0.1:15432/app"
npm run db:migrateUse a short-lived rstream credential for the CI job, and use a database role scoped to the migration or maintenance task. Install PostgreSQL client tools in the CI image so the job can wait on pg_isready before it starts migrations. rstream controls the network path, while PostgreSQL controls database identity and authorization.
Operate several private databases
Use stable tunnel names that encode the environment and service.
prod-postgres-primary
staging-postgres
analytics-readonly
customer-support-readonlyThen open the client port you need.
rstream nc -L 127.0.0.1:15432 -R rstrm://staging-postgres
rstream nc -L 127.0.0.1:15433 -R rstrm://analytics-readonlyList private database tunnels by name.
rstream tunnel list --filter 'name=*postgres*' -o json |
jq -r '.[] | [.name, .type, .status, .client_id] | @tsv'For team workflows, keep a small mapping in your runbook.
| Tunnel name | Local port | Database role |
|---|---|---|
staging-postgres | 15432 | app |
analytics-readonly | 15433 | analytics_reader |
customer-support-readonly | 15434 | support_reader |
The tunnel name identifies the network path, and the database role defines what the connected user may do after the connection reaches PostgreSQL.
Security notes
Keep PostgreSQL bound to localhost, a private LAN address, or a private VPC address, and create a private rstream tunnel for direct database access. Reserve published TCP or TLS exposure for deliberately public protocol gateways with their own access policy and audit model.
Use dedicated database users for tunneled workflows. A developer inspection role, a migration role, and a readonly support role should have different PostgreSQL grants. Protect sensitive data with both the rstream network policy and PostgreSQL's own authentication, grants, and audit trail.
Use short-lived rstream credentials for automation. For hosted projects, fine-grained tunnel grants can restrict which clients may create, list, or connect to the private tunnel. For self-hosted deployments, keep engine tokens scoped and rotated according to your operating model.
When you are done with the sample database, clean it up.
make clean