Skip to main content

PostgreSQL

Every deployment has its own PostgreSQL 18 database and a role that owns it.

Connecting​

The connection details are in the environment:

VariableContents
DATABASE_URLpostgresql://<user>:<password>@<host>:<port>/<database>
PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASEThe same, for libpq-based tools

Read them from the environment; the database and role names are not predictable, and the platform can change the password.

pg
import pg from 'pg'

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
await pool.query('CREATE TABLE IF NOT EXISTS visits (id serial PRIMARY KEY)')

Connection pooling​

Connections go through a pooler in transaction mode. A server connection is assigned per transaction, not per client connection. As a result:

  • Session state does not persist between transactions. Do not rely on session-level SET, LISTEN/NOTIFY, session-level advisory locks, temporary tables or WITH HOLD cursors.
  • Prepared statements are supported.
  • Up to 50 client connections per database. Transactions from all of them share a small pool of server connections, so keep transactions short.

Permissions​

The role owns its database. It can create schemas, tables, indexes and other objects, and install trusted extensions such as pgcrypto, uuid-ossp and citext.

It cannot create other databases, install extensions that require superuser, or connect to other databases.

Settings​

These settings apply to every session:

SettingValueCan be changed per session
statement_timeout30 sYes
idle_in_transaction_session_timeout60 sYes
temp_file_limit64 MBNo

Because of transaction pooling, change a setting inside the transaction that needs it, with SET LOCAL:

BEGIN;
SET LOCAL statement_timeout = '5min';
-- long-running statement
COMMIT;

Migrations​

There is no release phase. Run migrations in the start command, before the server starts. See Runtime contract: Processes.

Size allowance​

The database may use 100 MiB.

UsageEffect
80 %, 90 %The account owner receives an email
100 %The database becomes read-only; writes fail with cannot execute INSERT in a read-only transaction
150 %Connections are refused

Usage is measured every minute. When usage falls below a threshold, its restriction is lifted at the next measurement.

Deleting rows does not reduce the database size; VACUUM FULL on the affected tables does. While the database is read-only, both are refused unless the transaction is explicitly read-write:

BEGIN;
SET TRANSACTION READ WRITE;
DELETE FROM events WHERE created_at < now() - interval '90 days';
COMMIT;

VACUUM FULL cannot run inside a transaction block; run it in a session that has set default_transaction_read_only = off.

freepod db status

shows the database name, role and current usage. --show-password prints the password.

Access from your machine​

These commands need a registered SSH key and ssh:

CommandEffect
freepod shell psqlRuns psql against the database; no local client needed
freepod db shellInteractive psql session
freepod db proxyForwards a local port to the database and prints a connection URL, for local tools and IDEs

psql, pg_dump, pg_restore and pg_isready run on the platform when invoked through freepod shell, whether or not the application image contains them.

Backups​

Freepod has processes in place to protect the platform's data and its users' data. It does not restore individual databases or buckets on request. Deployments whose data matters should keep their own backups.

To back up:

freepod shell pg_dump > backup.sql
freepod shell pg_dump -Fc > backup.dump # custom format

Restore:

freepod shell psql < backup.sql
freepod shell 'pg_restore -d "$PGDATABASE"' < backup.dump

Quote arguments that reference variables, such as $PGDATABASE, so they are expanded on the platform rather than by the local shell.