Skip to content

Cloudflare Workers with Postgres: setup, costs, and migration

10 min readCloudflarePostgresServerlessSupabaseDatabases

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 appWhere I would start
New Worker app, comfortable with SQLite and the D1 bindingEvaluate D1
Existing Postgres schema, queries, extensions, or migrationsKeep Postgres and connect through Hyperdrive
Existing Supabase app using its auth, storage, and realtime APIsKeep those services while you move the app; decide on the database separately
Postgres app whose database bill or hosting workflow needs to changeCompare 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.

ComponentFree optionPaid pricing to know
Workers100,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
Hyperdrive100,000 database statements/dayIncluded in Workers Paid, with no separate query charge. Other Workers, Hyperdrive, and database limits still apply. Source
Layerbase PostgresDevelopment/testing plan: two databases, 5 GB storage, sleeping databasesSolo is $5/month; Pro is $15/month. Capacity, storage, connection, and branch limits apply. Source
Existing Supabase databaseKeep the plan you already useSupabase 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:

sql
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:

bash
pnpm exec wrangler login
pnpm exec wrangler hyperdrive update YOUR_HYPERDRIVE_ID --caching-disabled

Copy 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:

json
{
  "name": "storefront-worker",
  "private": true,
  "type": "module"
}

Install the driver and CLI:

bash
pnpm add pg@8.23.1
pnpm add -D wrangler@4.150.0 @types/pg

Save wrangler.jsonc, replacing the ID with your actual Hyperdrive configuration ID:

json
{
  "$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:

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:

bash
pnpm exec wrangler types

4. 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:

bash
(
  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:

bash
curl http://localhost:8787

With the schema above in a fresh database, the response is:

json
[{"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:

bash
pnpm exec wrangler deploy

Request 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 appWhat to plan
Postgres tables and dataCopy and verify them; update the database connection
Auth usersImport if appropriate, then configure the replacement login flow; copying users does not provide Supabase's hosted auth endpoints
Active sessionsPlan for users to sign in again after a full auth cutover
StorageMove files separately, or keep the existing storage service
Edge FunctionsPort the application code and configuration to the new runtime
Realtime subscriptionsKeep the service or build a replacement; Hyperdrive does not provide realtime subscriptions
Row-level securityReview 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.