Cloudflare Workers with Postgres: setup, costs, and migration
Moving an app to Cloudflare does not mean its database has to move too. If the app already uses Postgres, start by connecting that database to the Worker. Keep the schema and queries you already have while you get the app running on its new host.
Hyperdrive is Cloudflare's connection layer for that setup. It pools connections to your database and can cache query results. It does not store your Postgres database for you. The database can stay on Supabase, Neon, DigitalOcean, or another Postgres host. Cloudflare's support matrix covers supported engines and features.
I build Layerbase Cloud. If you want to change the database host as well, our offer is managed Postgres with a flat monthly database bill, branching, and other engines in the same account. This guide uses a small product catalog to show the connection setup, then separates the costs and the parts of a Supabase migration that still need work.
D1 or Postgres with Hyperdrive?
D1 is Cloudflare's managed database with SQLite SQL semantics. Hyperdrive connects a Worker to an existing database. They solve different parts of the setup.
| Your app | Where I would start |
|---|---|
| New Worker app, comfortable with SQLite and the D1 binding | Evaluate D1 |
| Existing Postgres schema, queries, extensions, or migrations | Keep Postgres and connect through Hyperdrive |
| Existing Supabase app using its auth, storage, and realtime APIs | Keep those services while you move the app; decide on the database separately |
| Postgres app whose database bill or hosting workflow needs to change | Compare Postgres hosts, including Layerbase, without also changing SQL engines |
A Postgres-to-D1 move is a database-engine migration. Check schema and query compatibility before choosing it. A frontend hosting move can be much smaller than that.
Cloudflare also documents Supabase access from Workers, including using the Supabase client for its backend services. You do not have to leave Supabase to use Cloudflare.
What the setup costs
Prices in USD, checked against official sources on October 13, 2026. Keep three things separate: Worker compute, Hyperdrive, and the database.
| Component | Free option | Paid pricing to know |
|---|---|---|
| Workers | 100,000 requests/day, with a 10 ms CPU limit per invocation | $5/month minimum per account. Includes 10 million requests and 30 million CPU milliseconds/month; additional usage is metered. Source |
| Hyperdrive | 100,000 database statements/day | Included in Workers Paid, with no separate query charge. Other Workers, Hyperdrive, and database limits still apply. Source |
| Layerbase Postgres | Development/testing plan: two databases, 5 GB storage, sleeping databases | Solo is $5/month; Pro is $15/month. Capacity, storage, connection, and branch limits apply. Source |
| Existing Supabase database | Keep the plan you already use | Supabase Pro starts at $25/month, with compute credits and usage allowances; additional resources can add charges. Source |
Workers Paid plus Layerbase Solo has a $10/month base. Workers Paid plus Layerbase Pro has a $20/month base. Those are sums of the subscription prices, not a promise that the whole app costs that amount: Workers overages, auth, object storage, and other services can add to it.
Layerbase Solo's 768 MB always-on pool keeps one database awake. Pro includes a larger shared pool and up to ten databases, not unlimited dedicated capacity for each one. Free is for development, testing, and early-stage projects; a product in production earning recurring revenue belongs on Solo or Pro. Pick the database plan for the workload and the databases you need awake together.
Build a small Postgres-backed Worker
The example reads a public product catalog. Use a new test database for the walkthrough. A production API for private data also needs authentication and authorization.
1. Create the database and a table
Create a Layerbase Postgres database named storefront, using PostgreSQL 18 for this walkthrough. You can also use a test database on your existing Postgres host. Cloudflare currently lists Postgres 9.0-18.x as supported; check its matrix before selecting a newer or prerelease version.
Run this once in the database's SQL console:
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
INSERT INTO products (name) VALUES ('Example product');For a real application, use a database role with the access that application needs. The catalog reader only needs to read the catalog.
2. Create the Hyperdrive configuration
In your Cloudflare account, open Hyperdrive and create a configuration named storefront. Supply the database hostname, port, database name, user, and password, or paste the connection string.
For Layerbase, get the direct origin connection from the database's Connect dialog by turning the pooled connection option off. Hyperdrive will manage the connections between its service and the database. Keep TLS enabled. Cloudflare validates the credentials during configuration creation; resolve any connection error before deploying the Worker. Its getting-started guide covers creation and bindings.
For this walkthrough, disable Hyperdrive query caching so the catalog reads current data. After installing Wrangler in the next step, the documented setting is:
pnpm exec wrangler login
pnpm exec wrangler hyperdrive update YOUR_HYPERDRIVE_ID --caching-disabledCopy the actual configuration ID from Cloudflare. The ID identifies the binding; the origin password stays in the Hyperdrive configuration and does not belong in the Worker source.
If the origin is Supabase, Cloudflare specifically recommends its Direct connection URL, rather than a Supabase pooler URL. The Supabase connection example explains that choice.
3. Create the Worker project
Create a directory named storefront-worker and save this as package.json:
{
"name": "storefront-worker",
"private": true,
"type": "module"
}Install the driver and CLI:
pnpm add pg@8.23.1
pnpm add -D wrangler@4.150.0 @types/pgSave wrangler.jsonc, replacing the ID with your actual Hyperdrive configuration ID:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "storefront-worker",
"main": "src/index.ts",
"compatibility_date": "2026-10-13",
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "YOUR_HYPERDRIVE_ID"
}
]
}Cloudflare's current docs say Node.js compatibility is enabled by default for compatibility dates from August 4, 2026 onward. For an older project's compatibility date, follow its Node.js compatibility instructions. This example uses an October date.
Create src/index.ts:
import { Client } from 'pg'
type Env = {
HYPERDRIVE: { connectionString: string }
}
type Product = {
id: number
name: string
}
async function handleRequest(request: Request, env: Env): Promise<Response> {
if (request.method !== 'GET') {
return Response.json({ error: 'Use GET.' }, {
status: 405,
headers: { Allow: 'GET' },
})
}
const client = new Client({
connectionString: env.HYPERDRIVE.connectionString,
})
try {
await client.connect()
const result = await client.query<Product>(
'SELECT id, name FROM products ORDER BY id LIMIT 20',
)
return Response.json(result.rows, {
headers: { 'Cache-Control': 'no-store' },
})
} catch (error: unknown) {
console.error('Postgres request failed', {
kind: error instanceof Error ? error.name : 'unknown',
})
return Response.json({ error: 'Database unavailable.' }, { status: 503 })
} finally {
await client.end()
}
}
export default { fetch: handleRequest }The client connects to env.HYPERDRIVE.connectionString, which is supplied by the binding. It opens a client for the request and closes it afterward; Hyperdrive handles the origin connection pool. Avoid creating a client that lives across unrelated requests. Cloudflare's node-postgres example shows the driver pattern.
Generate runtime types for your editor:
pnpm exec wrangler types4. Run locally, then check the deployed path
For local development, enter the connection override at a hidden prompt. Use your local test database's URL, or a separate hosted test database with the TLS settings it requires:
(
printf 'Test database URL (hidden): '
IFS= read -r -s CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE || exit 1
printf '\n'
export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE
pnpm exec wrangler dev
)At the prompt, paste your test database's URL, for example postgresql://test_user:test_password@127.0.0.1:5432/storefront, with your actual credentials and port. The input is hidden and is read as input rather than entered as a command in shell history. The parentheses keep the exported variable in a subshell, so it does not remain set in your terminal after Wrangler stops. This works in Bash and Zsh. If you need to run Postgres locally, the Layerbase CLI guide covers creating one. Keep a hosted database password out of committed config files.
From another terminal:
curl http://localhost:8787With the schema above in a fresh database, the response is:
[{"id":1,"name":"Example product"}]Local development with this override connects directly to the database. It bypasses Hyperdrive's pooling and query caching, so a successful local request verifies the Worker and query, not the deployed Hyperdrive path. Cloudflare's local development docs make that distinction.
Once the local query works and the origin configuration has validated, deploy:
pnpm exec wrangler deployRequest the deployment URL printed by Wrangler. Confirm the same catalog response there, then check a database change and the next read. If the app needs an idle database to wake, exercise a cold connection too. Those checks cover behavior a local run does not.
Hyperdrive caching is a separate choice
Hyperdrive caches eligible reads by default. Writes do not invalidate those cached query results. A successful write followed by a cached read can therefore show the previous value. For inventory, permissions, account balances, and other reads that must see the new value, use a cache-disabled configuration. Cloudflare's caching guidance explains the behavior.
The example's Cache-Control: no-store controls the HTTP response. It does not disable Hyperdrive's SQL-result cache. That is why the configuration step disables caching separately.
For less time-sensitive data, evaluate caching deliberately. You can use separate Hyperdrive configurations for different freshness needs. Hyperdrive also has session-related limits: it does not support LISTEN/NOTIFY, advisory locks, or arbitrary session state. Check its supported-features list before moving code that depends on those features.
Leaving Supabase: separate the app move from the database move
Start with the smaller change. Deploy the app on Workers, keep Supabase, and verify the connection and the Supabase services the app uses. That may be the whole migration you need.
If you also want to move the database to Layerbase, use the Supabase migration flow. Our migration walkthrough covers the schema, data, and optional auth-user import, including preserving password hashes. That does not bring the entire Supabase platform along.
| Part of the app | What to plan |
|---|---|
| Postgres tables and data | Copy and verify them; update the database connection |
| Auth users | Import if appropriate, then configure the replacement login flow; copying users does not provide Supabase's hosted auth endpoints |
| Active sessions | Plan for users to sign in again after a full auth cutover |
| Storage | Move files separately, or keep the existing storage service |
| Edge Functions | Port the application code and configuration to the new runtime |
| Realtime subscriptions | Keep the service or build a replacement; Hyperdrive does not provide realtime subscriptions |
| Row-level security | Review the database role and how the Worker supplies authorization context |
Connecting through a Postgres URL does not automatically reproduce the Supabase client's JWT-based request context. Review your RLS policies and the privileges of the connecting role before exposing private data. Supabase's RLS docs explain how that context and privileged roles affect access.
Where Layerbase fits
If keeping Supabase is working, you can finish the hosting move there. If the database bill or workflow is what you want to change, Layerbase gives you managed Postgres inside a fixed plan and database branches for testing changes against separate writable data.
A Redis cache or a separate search engine can join the same account as the stack grows, within the plan's engine and capacity limits. Hyperdrive connects the Postgres part of this example; use each other engine's appropriate client and connection path.
For a new app, create a Postgres database and work through the connection before adding more services. For an existing Supabase app, start the migration only when changing the database host solves a problem you actually have.
FAQ
Can Cloudflare Workers use Postgres?
Yes. Hyperdrive connects Workers to a supported Postgres database and supplies a connection binding for the driver. The database remains on its existing host, and you can use the node-postgres client as shown here.
Is Hyperdrive a Postgres database?
Hyperdrive is the connection, pooling, and optional caching layer. A separate provider hosts the database. Cloudflare also offers PlanetScale databases through its dashboard; that is a separate database product and bill. See the Cloudflare/PlanetScale setup.
Do I have to replace Postgres with D1?
No. D1 is a SQLite-based option for an app that fits that model. Existing Postgres apps can keep Postgres and connect through Hyperdrive.
Can I keep Supabase Auth after moving the app to Workers?
You can keep calling Supabase's services from the app on its new host. A separate database or auth migration is a separate decision. If you replace the auth service too, plan the login flow, session cutover, and authorization rather than treating an auth-user copy as a complete backend migration.
Does the local example test Hyperdrive's production cache?
The local connection override bypasses Hyperdrive's cache and pool. It checks the Worker and the SQL against your test database. Verify the deployed configuration separately, especially the reads whose freshness matters.
Verification note
Provider facts and prices were checked against official sources on October 13, 2026. The TypeScript example was type-checked, bundled without deployment, and run with Wrangler 4.150.0, node-postgres 8.23.1, and a disposable local Postgres database using the local connection override. The GET returned the catalog row; POST returned 405. That validation covers the local runtime and query. The hosted Hyperdrive-to-Layerbase path, query cache, cold starts, and production latency need a deployed check; no live benchmark is claimed here.
Keep reading
- Supabase is acquiring Turso. Read this if you build on your own.Supabase announced it is buying Turso, the company behind libSQL, alongside another $150M. Nothing breaks today. The part worth your attention is how few companies now own the default database for small projects, and what that does to pricing later.
- Sleep vs Pause vs Archive: Why Free Databases Fail DifferentlyA sleeping database wakes on your next query, a paused project waits for a human to click Resume, and an archived one waits for you to ask for it back. The difference decides whether an idle week costs you three seconds or an afternoon of debugging the wrong layer.
- Supabase alternatives: what to use when the meter stops making senseSupabase is a good product with a metered bill and a Postgres-only ceiling. Here is the honest shortlist of alternatives, what each one is actually for, and when you should just stay.
- Supabase alternatives for Lovable appsLovable defaults to Supabase. Here is the honest shortlist of what else to use for the database layer, and when each one is the right pick.