Skip to content

Postgres

Projects use D1 by default. On Pro and Studio, a project can use Postgres instead: a Neon database in the project’s region, with each environment on its own branch.

Creating a Postgres project

POST /v1/projects with { "name", "engine": "postgres" }. Pro includes 1 Postgres project and Studio 3. Hobby can’t create one (403 engine_not_on_plan); past your plan’s count, creating returns 403 plan_limit_reached with details.limit set to postgresProjects.

eu projects run in Frankfurt and us projects in Ohio. Point-in-time history is kept for your plan’s backup window.

Your code doesn’t change

The same flatstack/schema.ts, functions and routes work on both engines. ctx.db is a Drizzle Postgres database, and you pass it the tables you define with table():

const db = ctx.db.asUser();
await db.insert(todos, { title: "Buy milk" });

const admin = ctx.db.asAdmin();
await admin.select().from(todos).where(eq(todos.done, false));
  • Access rules apply exactly as on D1. See Data and access rules.
  • .all(), .get() and .run() work as on D1. You can also just await a query.
  • asAdmin().transaction(async (tx) => …) runs the callback in one Postgres transaction.
  • Your app reaches its database through Cloudflare Hyperdrive. Reads always see the latest writes: query caching is off.

Compute

Postgres compute is measured in CU-hours: one compute unit running for one hour. Compute scales between 0.25 and 2 CU with load and suspends after 5 minutes without queries, so an idle database uses none. The first query after a pause wakes it, which takes a moment.

Compute hours are pooled across the account: 120 CU-h a month on Pro, 360 on Studio.

Usage What happens
80% Email and a dashboard banner.
100% Compute is held at 0.25 CU for the rest of the period. Everything still works, more slowly under load.
150% The database is read-only until the period resets, you buy a compute pack or you upgrade. Writes return 429 postgres_read_only; reads continue.

Changes take effect within about 5 minutes, when usage is next measured. Every change is in the audit log.

Always-on

The always-on add-on (£15 a month per project) keeps that project’s compute running, so it never pauses, and gives it 500 CU-h of its own. Its own hours are used first; only the rest counts towards the pool. While its own hours last, the pool’s caps don’t apply to it. See Billing for add-ons.

Migrations

flatstack deploy and POST /v1/projects/{id}/migrations apply migrations to Postgres in one transaction: if any statement fails, none of them apply, and the response is 422 migration_failed with the database’s error. See Migrations.

Connecting from other tools

For BI tools, migrations from your own scripts or a one-off psql, account owners can get a connection string. Admins, developers, viewers and access tokens can’t, whatever their scopes.

Request Who What it does
GET /v1/projects/{id}/database Everyone Engine, compute state (normal, minimum or read_only) and whether it’s always-on. No host or credentials.
GET /v1/projects/{id}/database/connection Owners Host, port, database, user, when the password was last set, and the IP allowlist. Never the password.
POST /v1/projects/{id}/database/connection/rotate Owners Sets a new password and returns { "connectionString" }.
PUT /v1/projects/{id}/database/allowed-ips Owners { "ips": [...] }. Only these addresses may connect.

The connection string is shown once. Flatstack doesn’t keep it, so rotating is the only way to get one. Each rotation stops the previous password working, so update your tools when you rotate. Your app isn’t affected: it moves to the new password straight away.

The connection string is for the production environment and needs TLS (sslmode=require). It connects to the same database your app uses, so the compute caps above apply to it too.

IP allowlist

Send IPv4 or IPv6 addresses, or CIDR ranges like 203.0.113.0/24, up to 50 of them. Send [] to allow any address again. Your app keeps reaching its database whatever you set: Flatstack’s own Cloudflare ranges are always allowed.

Errors

Code Status Meaning
engine_not_on_plan 403 Your plan has no Postgres. Upgrade to Pro or Studio.
plan_limit_reached 403 Your plan’s Postgres projects are in use.
postgres_engine_required 409 The project uses D1, so it has no connection string.
postgres_read_only 429 Compute reached 150% of the period’s hours.
forbidden 403 Connection strings and allowlists are for account owners, signed in.

As Markdown: apps/postgres.md