Postgres
A workspace can connect Postgres databases it already has, at Neon, Supabase, Amazon RDS or anywhere else the internet can reach, and give each one to the apps that need it. You give us the connection URL once. We keep the password encrypted and hand it only to Cloudflare Hyperdrive, which holds the connection to your database. Your app sends SQL to the platform over HTTP and never sees the password, the URL or anything else that would let it reach the database without us.
Team connects up to 3 databases per workspace and Business up to 10. Free has none. Each connection takes room we have with Cloudflare, so a new one can, rarely, wait up to a day while we make more. If your workspace moves to a plan with fewer, its oldest connections keep working, as many as the new plan allows, and apps can’t reach the rest until you remove some or move back. On Free, by cancelling or because a payment lapsed, apps can’t reach any of them, and after 30 days we take them off Cloudflare, so Cloudflare no longer holds the password. We keep each connection, encrypted as before, and move back to Team or Business and it goes on again by itself within the hour. See Limits.
Each app also has a SQLite database of its own, on every plan. Use Postgres when the data is already there, or when several apps share it.
Connect a database
Section titled “Connect a database”Workspace owners connect databases. Name the connection, then paste the URL at the hidden prompt:
jiayang postgres add acme mainOr pipe it in, from wherever you keep it, so it never lands in your shell history:
printf %s "$DATABASE_URL" | jiayang postgres add acme mainConnected app_user@ep-cool-name-a1b2c3.us-east-2.aws.neon.tech:5432/neondb to acme. The password is sealed and won't be shown again.Give it to an app with `jiayang postgres grant acme/<app> main`.The URL is the one your provider gives you, like postgres://user:password@host:5432/database. The name is what your apps call it: 1 to 32 lowercase letters, digits and dashes, starting with a letter. In the dashboard it’s under Postgres in the sidebar.
We try the database before we keep anything. If it doesn’t connect, nothing is saved and you’re told why:
- the host isn’t in public DNS, or it’s a private or internal address. Only databases that take connections from the internet can be connected, so
localhost,10.x,192.168.x,*.internaland the like are refused; - nothing answered, or the database refused the connection. Hyperdrive connects from Cloudflare’s IP ranges, so if your database has a list of allowed addresses, add them. A workspace’s fixed addresses don’t apply here: those ranges are shared by every Cloudflare customer, so prefer a strong password and TLS checked with
--tls verify-fullover the list; - the user name or password was turned down, or there’s no database by that name;
- TLS failed. Connections always use TLS. A URL with
sslmode=disableorsslmode=allowis refused.
To check the certificate against your provider’s certificate authority as well, give it with --tls verify-ca or --tls verify-full and --ca-file, the one certificate that signed your database’s:
jiayang postgres add acme main --tls verify-full --ca-file rds-ca.pemGive it to an app
Section titled “Give it to an app”jiayang postgres grant acme/payroll mainThe app can query it within 10 seconds, with nothing to redeploy. jiayang postgres revoke acme/payroll main takes it away, and the app’s queries stop within 10 seconds. jiayang postgres show acme/payroll lists what an app was given, and the app’s Secrets tab shows it too. Static sites run no code, so they can’t be given one.
Query it from your app
Section titled “Query it from your app”Send SQL to http://postgres.jiayang.internal/<name>/query with POST, and the header x-jiayang-postgres: 1. It works the same from a Worker and from a container. The host only exists inside your app: the platform answers it, and it reaches nothing anywhere else.
async function sql(text, params = []) { const res = await fetch("http://postgres.jiayang.internal/main/query", { method: "POST", headers: { "content-type": "application/json", "x-jiayang-postgres": "1" }, body: JSON.stringify({ sql: text, params }), }); const body = await res.json(); if (!res.ok) throw new Error(body.message); return body.rows;}
const open = await sql("SELECT id, title FROM todos WHERE done = $1 ORDER BY id LIMIT 50", [false]);From Python in a container:
import jsonimport urllib.request
def sql(text, params=()): req = urllib.request.Request( "http://postgres.jiayang.internal/main/query", data=json.dumps({"sql": text, "params": list(params)}).encode(), headers={"content-type": "application/json", "x-jiayang-postgres": "1"}, method="POST", ) with urllib.request.urlopen(req) as res: return json.load(res)["rows"]Put values in params and refer to them as $1, $2 and so on. Never paste them into the SQL. One request runs one statement: two in one string are refused.
The answer:
{"rows": [{"id": 1, "title": "Write the docs"}], "columns": [{"name": "id", "type": 20}, {"name": "title", "type": 25}], "command": "SELECT", "row_count": 1}type is the column’s Postgres type id. Booleans, smaller integers, floats, json and jsonb come back as JSON values. Everything else, bigint, numeric, dates and times included, comes back as the text Postgres writes, so nothing loses precision.
To run several statements in one transaction, send them to /<name>/batch. If one fails, none of them stay:
{"statements": [ {"sql": "UPDATE accounts SET balance = balance - $1 WHERE id = $2", "params": [100, 1]}, {"sql": "UPDATE accounts SET balance = balance + $1 WHERE id = $2", "params": [100, 2]}]}The answer is {"results": [...]}, one result for each statement.
Each request opens its own session, so session state doesn’t carry over from one request to the next: a SET, a temporary table or a transaction you begin yourself ends with the request. Use a batch for a transaction. Query caching is off, so a read always sees the latest writes.
Limits
Section titled “Limits”| Request body | 1 MB |
| Statements in a batch | 100 |
| Rows in an answer | 5,000, across a batch |
| Values in an answer | 25,000, rows times columns, across a batch |
| Size of an answer | 1 MB as JSON, names and escapes included, and 2 MB as the database sends it |
| Time a statement may run | 30 seconds |
| Calls per app | 1,000 in 10 seconds in each Cloudflare location the app runs in |
An answer that would go past a limit is refused whole, never cut short. Page through large results with LIMIT and OFFSET, or a WHERE on the last id you got.
Errors
Section titled “Errors”Every error is JSON with error and message:
| Status | error |
Meaning |
|---|---|---|
| 400 | query_failed |
Postgres refused the statement. code is its SQLSTATE, with detail, hint and position when it gave them. |
| 400 | bad_request |
The body isn’t a statement we can read. |
| 403 | forbidden |
The request didn’t carry x-jiayang-postgres: 1, or the app’s list of hosts is on and doesn’t include the database’s host. |
| 403 | plan_limit |
The workspace has more connections than its plan allows, and this one isn’t among the oldest it keeps. Remove a connection or change the plan. |
| 404 | no_connection |
The app wasn’t given a connection by that name. |
| 413 | too_large |
The request or the answer is past a limit above. |
| 429 | rate_limited |
The app sent more than its share. Wait the seconds in retry-after. |
| 503 | unavailable |
We couldn’t reach the database. After the database turns a connection away, we wait the seconds in retry-after, about 10, before trying it again. A workspace owner can run jiayang postgres check. |
| 503 | not_ready |
The connection is new and still being set up. Try again in a minute. |
| 503 | stopped |
The app is stopped, so it can’t reach its databases. |
| 503 | timed_out |
The statement ran past 30 seconds and was stopped. |
If the app’s list of hosts is on, add the database’s host to it, like ep-cool-name-a1b2c3.us-east-2.aws.neon.tech. Until you do, its queries are refused, and jiayang postgres grant, jiayang postgres show and the dashboard say so.
Change the password
Section titled “Change the password”Change it at your database first, then give it to us. Paste it at the hidden prompt, or pipe it in:
jiayang postgres rotate acme mainWe try the new password before we switch, so a wrong one changes nothing, and apps keep working with nothing to redeploy. To point a connection at a different database, give it a whole new URL with jiayang postgres update acme main --url. --tls and --ca-file change how its certificate is checked.
A connection is tried at most once in 10 seconds, by a check or a change. A workspace has 30 tries an hour, adds, changes and checks together, the ones that didn’t connect included.
Check a connection
Section titled “Check a connection”jiayang postgres check acme mainCouldn't connect to main. The database turned down the user name or password.jiayang postgres list acme shows every connection, the apps given each, and how its last check went. Owners and admins can check a connection; only owners can add, change or remove one, or give it to an app.
Remove a connection
Section titled “Remove a connection”jiayang postgres remove acme mainEvery app given it stops reaching it within 10 seconds. Your database and its data aren’t touched. It asks first, and where there’s no terminal to ask at, such as in a script, it needs --yes.
Who can do what
Section titled “Who can do what”| Owners | Admins | An app’s editors | Members | |
|---|---|---|---|---|
| See the workspace’s connections | Yes | Yes | No | No |
| Check a connection | Yes | Yes | No | No |
| Add, change, rotate or remove one | Yes | No | No | No |
| Give one to an app, or take it away | Yes | No | No | No |
| See what an app was given | Yes | Yes | Yes | No |
Nobody can read a password back, owners included. Every change is in the audit log as postgres.add, postgres.update, postgres.check, postgres.remove, postgres.grant or postgres.revoke, with the connection’s name, host, database and user, and never the password. A database that didn’t connect when someone added or changed one is there too, as postgres.refused, with where it went and why. Webhooks can send all of them but postgres.check and postgres.refused. A workspace’s export lists its connections and what each app was given, without passwords.
An agent can do the same through jiayang mcp, with the permissions of the person it acts as.
How it counts
Section titled “How it counts”Each statement your apps run on a connected database counts as 2,000 database rows read, toward the same allowance and price as your apps’ own database rows. A batch counts each statement in it, and its transaction’s start and end. Your workspace’s usage shows each connection on a row of its own, as Postgres: main, since a connection belongs to the workspace rather than to one app. Hyperdrive counts statements by the hour, so they show up within the hour.
What your database provider charges for its own service is between you and them.
Where your data goes
Section titled “Where your data goes”Your SQL and its results pass through Cloudflare, between your app and your database, and aren’t stored or logged on the way. The password is encrypted in our control plane and given only to Cloudflare Hyperdrive, which keeps it to open connections to your database. See Security.