# 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()`:

```ts
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](data.md).
- `.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](../api/billing.md) 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](migrations.md).

## 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. |