BuildWithMatija
  1. Home
  2. Blog
  3. Cloudflare
  4. Payload CMS on Cloudflare: Complete D1 + R2 Setup Guide

Payload CMS on Cloudflare: Complete D1 + R2 Setup Guide

How to provision D1 and R2, configure Wrangler bindings, and ensure Payload migrations write to the remote database.

11th August 2026·Updated on:13th August 2026··
Cloudflare
Payload CMS on Cloudflare: Complete D1 + R2 Setup Guide

Comparing Headless CMS Options?

Answer 10 simple questions and get an independent recommendation matched to your project, budget, and team structure.

Try the CMS PickerGet a Second Opinion

☁️ Cloudflare Edge Development Guides

Complete Cloudflare guides with practical examples, deployment strategies, and developer prompts to help you build and ship edge applications faster.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Contents

  • 1. Understand the architecture
  • If Payload doesn't run on Cloudflare Workers
  • Cost considerations
  • 2. Prerequisites
  • 3. Create a Cloudflare API token
  • 4. Store local CLI credentials safely
  • 5. Choose resource and binding names
  • 6. Create the D1 database
  • 7. Create the private R2 bucket
  • 8. Configure `wrangler.jsonc`
  • 9. Generate Cloudflare binding types
  • 10. Enable uploads in Payload
  • 11. Configure Payload for D1 and R2
  • 12. Create Payload migrations
  • 13. Apply migrations locally
  • 14. Apply migrations remotely
  • 15. Verify the remote database
  • 16. Verify R2 through Payload
  • 17. Configure runtime secrets
  • 18. Recommended verification commands
  • 19. Common failures
  • `Invalid access token [code: 9109]`
  • Authentication works, but resource creation is forbidden
  • Payload says the migration succeeded, but remote D1 is empty
  • Files are written to local disk instead of R2
  • Private files return 403 or 404
  • A resource already exists
  • Local development unexpectedly touches production
  • `wrangler deploy` rejects the build for exceeding the size limit
  • 20. Production checklist
  • FAQ
  • Conclusion
  • Further reading
On this page:
  • 1. Understand the architecture
  • 2. Prerequisites
  • 3. Create a Cloudflare API token
  • 4. Store local CLI credentials safely
  • 5. Choose resource and binding names
Build with Matija logo

Build with Matija

Senior-led B2B websites, applications, content systems, and digital infrastructure. Business-first, full-stack, AI-assisted, no handoffs.

Services

  • B2B Website Development
  • CMS Architecture Review & Platform Blueprint
  • Next.js + Payload Advisory
  • AI Integration & Implementation

Resources

  • CMS Hub
  • B2B Website Strategy
  • E-commerce Hub
  • Blog
  • Case Studies

Payload CMS

  • Payload CMS Developer
  • Payload CMS Migration
  • Payload CMS Demos
  • All Payload CMS Resources

Discuss your project

Planning a rebuild, migration, application, workflow change, or platform decision? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

Running Payload entirely on Cloudflare means swapping a database connection string and a disk-based upload folder for two native bindings: D1 for the database and R2 for file storage. This guide covers the full setup, from creating the Cloudflare resources to wiring the bindings into a Payload config that keeps working across local development, CLI migrations, and a deployed Worker. The part most guides skip is getting remote: true and remoteBindings configured correctly, so a migration that reports success actually writes to the real database instead of a local Miniflare simulation.

I set this up while building a personal app for tourist registration and tax filing. The project was small enough that keeping the whole stack on Cloudflare made sense: one dashboard, one platform, no separate database or storage bill to manage. The part that took the most trial and error involved keeping Cloudflare bindings available in three different contexts at once: the deployed Worker, local Next.js development, and Payload's own CLI commands for migrations. Getting that wrong lets Payload report a clean migration while the remote database stays empty. This guide walks through the setup that avoids that failure mode.

Tested with payload@3.88.0, @payloadcms/db-d1-sqlite@3.88.0, @payloadcms/storage-r2@3.88.0, and the current Wrangler CLI. Last updated August 13, 2026.

This guide shows how to replace a traditional database server and local upload directory with Cloudflare infrastructure:

  • Cloudflare D1 provides a managed, serverless SQLite database for Payload documents.
  • Cloudflare R2 provides S3-compatible object storage for Payload uploads.
  • Cloudflare Workers runs the deployed application and receives D1 and R2 through native bindings.
  • Wrangler creates, configures, migrates, and verifies the Cloudflare resources.

The examples use Payload CMS, Next.js, OpenNext, pnpm, and a private R2 bucket. Adapt the names to your application.

1. Understand the architecture

With a conventional deployment, Payload often connects to PostgreSQL or MongoDB over a URL and writes uploaded files to a persistent disk or an S3-compatible service. The Cloudflare-native alternative uses bindings instead:

text
Browser
  |
  v
Next.js + Payload on Cloudflare Workers
  |-- D1 binding --> Cloudflare D1 (documents, users, relationships, migrations)
  `-- R2 binding --> Cloudflare R2 (images, PDFs, imports, generated files)

Bindings are capabilities supplied to the Worker at runtime. The application does not need a D1 connection string or R2 access keys when it runs inside Cloudflare.

Use this architecture when:

  • the application can use SQLite semantics;
  • the expected workload fits D1's limits and single-database write model;
  • uploads should survive stateless Worker deployments;
  • keeping the database, object storage, and application on Cloudflare is useful.

Before choosing D1, review its current limits. D1 is not a drop-in replacement for every PostgreSQL or MongoDB workload. For a similar production setup on a different cloud, see Payload CMS on Azure: Production Architecture Guide.

If Payload doesn't run on Cloudflare Workers

D1 is the part of this stack that decides whether the architecture fits at all. R2 is portable: its S3-compatible API works from any host, including a Payload app deployed on Vercel. D1 does not offer that flexibility. Payload's official D1 adapter requires a native Cloudflare Worker binding, and Vercel cannot supply one. Calling D1 through its REST API instead of the binding is possible but unsuitable as Payload's primary database interface.

OptionWhen to useTrade-off
Keep Payload on VercelYou want to stay on VercelUse PostgreSQL, such as Neon, with a private Cloudflare R2 bucket instead of D1
Move Payload to Cloudflare WorkersYou want D1 and R2 togetherRequires deploying Payload itself to Workers, which the rest of this guide covers
Build a custom D1 proxy/adapterYou need D1 specifically but can't leave VercelTechnically possible, adds real complexity, not recommended

For a Vercel-hosted Payload app, Neon Postgres paired with a private Cloudflare R2 bucket covers the same object-storage benefit without the Worker-binding requirement.

Cost considerations

Running Payload on Workers means the account needs the Workers Paid plan, which starts at $5/month and includes 10 million requests and 30 million CPU milliseconds per month, along with a 10 MiB compressed bundle limit instead of the free plan's 3 MiB. Usage beyond those allowances costs $0.30 per million additional requests and $0.02 per million additional CPU milliseconds, and static asset requests don't count against the request quota. A small Payload application typically stays close to the $5 base, plus whatever D1 and R2 usage runs past their own included allowances. See Cloudflare's official Workers pricing and Worker limits for current figures.

2. Prerequisites

You need:

  • a Cloudflare account;
  • Node.js and pnpm;
  • an existing Payload 3 project;
  • an upload-enabled Payload collection such as media;
  • a Worker-compatible deployment, such as OpenNext for a Next.js application.

Install Wrangler locally so the project controls its CLI version:

sh
pnpm add -D wrangler@latest
pnpm exec wrangler --version

Cloudflare recommends a project-local Wrangler installation. See the official Wrangler installation guide.

Install the Payload and OpenNext integrations:

sh
pnpm add @payloadcms/db-d1-sqlite @payloadcms/storage-r2 @opennextjs/cloudflare

Keep all @payloadcms/* packages on exactly the same version as payload.

3. Create a Cloudflare API token

Wrangler can use browser-based OAuth with pnpm exec wrangler login, but an API token is more convenient for repeatable CLI and CI workflows.

  1. Open the Cloudflare dashboard.
  2. For a user token, go to My Profile → API Tokens. For an account-owned service token, go to Manage Account → API Tokens.
  3. Select Create Token, then create a custom token.
  4. Give it a descriptive name, such as payload-d1-r2-provisioning.
  5. Add these account permissions:
    • D1 Edit
    • Workers R2 Storage Write
    • Add Workers Scripts Write only if the same token will deploy the Worker.
  6. Restrict the token to the intended Cloudflare account.
  7. Add an expiration date when practical.
  8. Create the token and copy it immediately. Cloudflare shows the secret only once.

The authoritative permission names are listed in Cloudflare's API token permissions, and the dashboard flow is covered in Create API token.

Never paste a token into source files, wrangler.jsonc, screenshots, issues, or chat logs.

4. Store local CLI credentials safely

Add .env to .gitignore:

gitignore
.env
.env.*
!.env.example

Create .env:

dotenv
CLOUDFLARE_API_TOKEN=<YOUR_API_TOKEN>
CLOUDFLARE_ACCOUNT_ID=<YOUR_ACCOUNT_ID>
PAYLOAD_SECRET=<A_LONG_RANDOM_APPLICATION_SECRET>

CLOUDFLARE_ACCOUNT_ID is optional when the token exposes only one account, but setting it removes ambiguity in automation.

Commit a safe .env.example instead:

dotenv
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
PAYLOAD_SECRET=

Wrangler supports CLOUDFLARE_API_TOKEN and loads project .env files. See Wrangler system environment variables.

Verify authentication:

sh
pnpm exec wrangler whoami

If it reports Invalid access token, create a new token rather than repeatedly reusing or broadening an exposed credential.

5. Choose resource and binding names

Use different names for the remote resources and the bindings used by code:

PurposeExample remote nameBinding used in code
D1 databasemy-payload-appD1
R2 bucketmy-payload-app-privateR2

The remote name identifies a resource in Cloudflare. The binding is the property available to the Worker, such as cloudflare.env.D1.

The examples below use:

sh
APP_NAME=my-payload-app
BUCKET_NAME=my-payload-app-private

Use explicit names in CI instead of relying on shell variables if your automation environment does not preserve them between steps.

6. Create the D1 database

First check whether the database already exists:

sh
pnpm exec wrangler d1 list

Create it when no exact-name match exists:

sh
pnpm exec wrangler d1 create my-payload-app --location weur

Location hints include weur, eeur, apac, oc, wnam, and enam. If legal or compliance requirements demand that data remain in the European Union, consider --jurisdiction eu instead of a location hint. Confirm the current behavior in the d1 create reference.

Wrangler prints a UUID. Save it for the next step:

text
database_id = <D1_DATABASE_UUID>

Do not create a second database if a previous attempt already created the intended exact-name resource. List first, then reuse its ID.

7. Create the private R2 bucket

List existing buckets:

sh
pnpm exec wrangler r2 bucket list

Create the bucket when it does not exist:

sh
pnpm exec wrangler r2 bucket create my-payload-app-private --location weur

Verify it:

sh
pnpm exec wrangler r2 bucket info my-payload-app-private

A new R2 bucket is private unless you explicitly enable an r2.dev URL or attach a public custom domain. For protected Payload uploads, leave the bucket private and let Payload enforce collection access control.

Wrangler does not need S3 access keys for these commands; S3 credentials only come into play when an external S3-compatible client accesses R2 directly. See Cloudflare's R2 CLI guide. If you ever need a Next.js route that talks to R2 directly instead of through Payload's storage adapter, How to Upload Files to Cloudflare R2 in a Next.js App covers that pattern.

8. Configure wrangler.jsonc

Add the bindings to the Worker configuration:

jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-payload-app",
  "main": ".open-next/worker.js",
  "compatibility_date": "<CURRENT_DATE>",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS",
  },
  "d1_databases": [
    {
      "binding": "D1",
      "database_name": "my-payload-app",
      "database_id": "<D1_DATABASE_UUID>",
      "remote": true,
    },
  ],
  "r2_buckets": [
    {
      "binding": "R2",
      "bucket_name": "my-payload-app-private",
      "remote": true,
    },
  ],
}

The remote: true properties matter for CLI processes that use Wrangler's platform proxy. Without them, a Payload migration can report success while writing only to .wrangler/state, leaving the remote D1 database empty.

The Payload configuration below deliberately disables remote bindings during normal local development and enables them when NODE_ENV=production. A deployed Worker always receives its real production bindings.

9. Generate Cloudflare binding types

Generate a TypeScript interface from the Wrangler configuration:

sh
pnpm exec wrangler types --env-interface CloudflareEnv

The result should contain equivalents of:

ts
interface CloudflareEnv {
  D1: D1Database
  R2: R2Bucket
  PAYLOAD_SECRET: string
}

Regenerate these types whenever binding names or Wrangler variables change. OpenNext documents this workflow in Bindings.

10. Enable uploads in Payload

R2 storage applies to upload-enabled collections. A minimal collection looks like this:

ts
import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
  slug: 'media',
  access: {
    read: ({ req }) => Boolean(req.user),
  },
  fields: [
    {
      name: 'alt',
      type: 'text',
    },
  ],
  upload: {
    mimeTypes: ['image/*', 'application/pdf'],
  },
}

Payload automatically adds file metadata fields and upload operations to an upload-enabled collection. See Payload uploads.

Do not set disablePayloadAccessControl: true for private files. Keeping the default behavior lets requests pass through Payload's read access control instead of exposing direct public R2 URLs.

11. Configure Payload for D1 and R2

The important configuration challenge is obtaining Cloudflare bindings in three contexts:

  1. a deployed Worker;
  2. local Next.js development;
  3. Payload CLI commands such as migrate and generate:types.

Use getCloudflareContext() inside the deployed OpenNext application and Wrangler's getPlatformProxy() for local development and Payload CLI processes:

ts
import { sqliteD1Adapter } from '@payloadcms/db-d1-sqlite'
import { r2Storage } from '@payloadcms/storage-r2'
import { getCloudflareContext, type CloudflareContext } from '@opennextjs/cloudflare'
import { buildConfig } from 'payload'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { GetPlatformProxyOptions } from 'wrangler'

import { Media } from './collections/Media'
import { Users } from './collections/Users'
import { migrations } from './migrations'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

const payloadCLICommands = [
  'generate:types',
  'generate:importmap',
  'migrate',
  'migrate:create',
  'migrate:status',
]

const isPayloadCLI = process.argv.some((argument) => payloadCLICommands.includes(argument))
const isProduction = process.env.NODE_ENV === 'production'

const cloudflare =
  isPayloadCLI || !isProduction
    ? await getCloudflareContextFromWrangler()
    : await getCloudflareContext({ async: true })

export default buildConfig({
  admin: {
    user: Users.slug,
    importMap: {
      baseDir: path.resolve(dirname),
    },
  },
  collections: [Users, Media],
  secret: process.env.PAYLOAD_SECRET || '',
  typescript: {
    outputFile: path.resolve(dirname, 'payload-types.ts'),
  },
  db: sqliteD1Adapter({
    binding: cloudflare.env.D1,
    prodMigrations: migrations,
  }),
  plugins: [
    r2Storage({
      bucket: cloudflare.env.R2,
      collections: {
        media: true,
      },
    }),
  ],
})

// Keep Wrangler out of the deployed worker bundle. This branch is used only by
// Payload CLI commands and local development.
function getCloudflareContextFromWrangler(): Promise<CloudflareContext> {
  return import(/* webpackIgnore: true */ `${'__wrangler'.replaceAll('_', '')}`).then(
    ({ getPlatformProxy }) =>
      getPlatformProxy({
        configPath: path.resolve(dirname, '../wrangler.jsonc'),
        environment: process.env.CLOUDFLARE_ENV,
        remoteBindings: isProduction,
      } satisfies GetPlatformProxyOptions),
  )
}

If more than one upload collection should use R2, add every collection slug:

ts
collections: {
  media: true,
  imports: true,
  generatedFiles: true,
}

The keys must exactly match registered Payload collection slugs. Payload's D1 adapter is documented under SQLite: D1 Database, and storage behavior is covered by Storage Adapters. If R2 does not fit your needs and you want a comparable setup with an object storage provider outside Cloudflare, Backblaze B2 Direct Uploads With Payload CMS Jobs walks through that adapter.

12. Create Payload migrations

After defining the collections, create a migration:

sh
pnpm payload migrate:create initial

Commit the generated migration files. Payload migrations are application code and should be reviewed like any other schema change.

For later schema changes:

sh
pnpm payload migrate:create describe_the_change

Avoid Wrangler's SQL migration generator for a Payload-managed schema, and let Payload generate and record its own migrations through @payloadcms/db-d1-sqlite instead.

13. Apply migrations locally

Normal local development uses Wrangler's local D1 simulation under .wrangler/state:

sh
pnpm payload migrate
pnpm dev

Keep .wrangler/ out of Git:

gitignore
.wrangler/

Local and remote D1 are separate databases. A successful local migration says nothing about the remote schema.

14. Apply migrations remotely

Run the Payload CLI with NODE_ENV=production. That makes getPlatformProxy() enable the configured remote bindings:

sh
cross-env NODE_ENV=production payload migrate
pnpm exec wrangler d1 execute D1 --remote --command "PRAGMA optimize"

For a cross-platform package script, install cross-env and add:

json
{
  "scripts": {
    "deploy:database": "cross-env NODE_ENV=production payload migrate && wrangler d1 execute D1 --remote --command 'PRAGMA optimize'"
  }
}

Then run:

sh
pnpm run deploy:database

Always take a backup or confirm D1 Time Travel coverage before applying a destructive production migration.

15. Verify the remote database

Check Payload's migration record:

sh
pnpm exec wrangler d1 execute D1 --remote \
  --command "SELECT * FROM payload_migrations ORDER BY id"

List application tables:

sh
pnpm exec wrangler d1 execute D1 --remote \
  --command "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name"

The output should include payload_migrations and tables derived from your Payload collections.

Cloudflare's D1 documentation also recommends explicit --local and --remote verification because they target different databases. See D1 getting started.

16. Verify R2 through Payload

Check the bucket from Wrangler:

sh
pnpm exec wrangler r2 bucket info my-payload-app-private

Then perform an application-level smoke test:

  1. Start or deploy the Payload application.
  2. Sign in to Payload Admin.
  3. Upload a small permitted file to the media collection.
  4. Confirm the Payload document contains the expected filename and metadata.
  5. Confirm the R2 bucket's object count increases.
  6. Download the file through Payload while authenticated.
  7. Confirm an unauthenticated request is rejected when the collection is private.
  8. Delete the test document and confirm the R2 object is removed.

This checklist verifies the full chain: Payload's access control, the R2 adapter, the Worker binding, and object deletion, confirming more than bucket existence alone. If you also serve uploads through a CDN in front of R2, cache headers need separate handling; Cloudflare CDN Cache Headers for Self-Hosted S3 Guide covers the Next.js and Payload specific fix for that.

17. Configure runtime secrets

The Cloudflare API token is a provisioning credential. The deployed application does not need it to use D1 or R2 bindings.

Add only application runtime secrets to the Worker:

sh
pnpm exec wrangler secret put PAYLOAD_SECRET

For CI/CD, store CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in the CI provider's encrypted secret store. Never commit them.

18. Recommended verification commands

Run these before deployment:

sh
pnpm exec wrangler whoami
pnpm exec wrangler d1 list
pnpm exec wrangler r2 bucket list
pnpm exec wrangler types --env-interface CloudflareEnv
pnpm exec tsc --noEmit
pnpm run test:int
pnpm run build:cloudflare

After deployment, repeat the remote D1 query and perform one authenticated upload/download/delete smoke test.

19. Common failures

Invalid access token [code: 9109]

The credential is malformed, expired, revoked, or is not a Cloudflare API token. Create a new API token and replace CLOUDFLARE_API_TOKEN. An R2 S3 secret access key is not interchangeable with a Cloudflare account API token.

Authentication works, but resource creation is forbidden

The token is valid but lacks D1 Edit or Workers R2 Storage Write, or it is restricted to a different account. Correct the token scope instead of granting unrelated permissions.

Payload says the migration succeeded, but remote D1 is empty

This almost always means the migration used local Miniflare storage.

Check all three conditions:

  1. D1 has "remote": true in wrangler.jsonc.
  2. getPlatformProxy() receives remoteBindings: true in production.
  3. The migration runs with NODE_ENV=production.

Then query with an explicit --remote flag. Do not trust migration logs alone.

Files are written to local disk instead of R2

Confirm that:

  • the collection has upload: true or an upload options object;
  • its slug appears in r2Storage({ collections: ... });
  • cloudflare.env.R2 exists in the generated binding types;
  • the R2 plugin is enabled in the environment being tested.

Private files return 403 or 404

Verify the collection's read access rule and that the request has a valid Payload session. Private R2 files are normally served through Payload so collection access control remains active.

A resource already exists

List existing D1 databases and R2 buckets, then reuse the exact intended resource. Avoid adding suffixes automatically: doing so can silently connect production to a new empty database or bucket.

Local development unexpectedly touches production

Ensure local calls to getPlatformProxy() use remoteBindings: false. Use separate Cloudflare environments and resource names for staging if developers need remote development.

wrangler deploy rejects the build for exceeding the size limit

Cloudflare's free plan caps a compressed Worker bundle at 3 MiB. The Workers Paid plan raises that ceiling to 10 MiB. Payload, Next.js, and dependencies such as XLSX processing, XML validation, or image processing add up quickly, and a real Payload application can land past the free-plan limit without doing anything unusual. Check the actual bundle size before assuming a quick trim will fix it: shaving a multi-megabyte gap out of a Payload plus Next.js bundle is rarely realistic. Upgrading to the Workers Paid plan through the Cloudflare dashboard's Workers plans page is usually the faster path to a working deployment.

20. Production checklist

  • The API token is scoped to one intended account and stored outside Git.
  • D1 and R2 names clearly identify the application and environment.
  • database_id matches the intended remote D1 database.
  • D1 and R2 binding names match the Payload configuration.
  • Remote migration behavior has been verified with a SQL query.
  • R2 remains private unless public access is an explicit product requirement.
  • Upload collection access rules have been tested anonymously and as an authenticated user.
  • PAYLOAD_SECRET is stored as a Worker secret.
  • Cloudflare binding types and Payload types are current.
  • Schema migrations are committed and reviewed.
  • Backup/Time Travel and recovery procedures are understood.
  • Exposed or temporary provisioning tokens have been rotated.
  • The Worker bundle size fits the account's plan limit (3 MiB free, 10 MiB paid).

FAQ

Can I use Cloudflare D1 with an existing Payload project that runs on PostgreSQL or MongoDB? Not as a drop-in swap. D1 is SQLite, so Postgres or MongoDB-specific queries, JSON operators, and migrations need rewriting for the @payloadcms/db-d1-sqlite adapter. Treat it as a data migration project, not a configuration change.

Do I need Cloudflare R2 access keys inside the Payload application itself? No. The Worker receives R2 through a native binding at runtime, so the application only needs cloudflare.env.R2. Access keys matter only if a separate tool outside Cloudflare needs S3-compatible access to the same bucket.

What happens if I forget to set remote: true in wrangler.jsonc? Local Wrangler processes, including Payload CLI migrations, write to .wrangler/state instead of the real Cloudflare resource. The migration command can report success while the remote D1 database and R2 bucket stay untouched.

Can this same Payload configuration run outside Cloudflare Workers, for example on a plain Node.js server? Only with changes. getCloudflareContext() and the D1/R2 bindings are Workers-specific. A Node deployment needs a different database adapter, such as Postgres or MongoDB, and a different storage adapter in place of this Cloudflare-native setup.

Does the D1 adapter support every feature available in Payload's PostgreSQL adapter? No. D1 runs on SQLite, so certain query patterns, extensions, and scaling characteristics differ from Postgres. Check Payload's SQLite documentation and D1's platform limits before committing a production workload to this stack.

Conclusion

This setup replaces a conventional database connection and disk-based upload folder with two Cloudflare bindings: D1 for Payload's documents and R2 for uploaded files. The configuration in this guide keeps those bindings available correctly across three contexts, the deployed Worker, local development, and Payload's own CLI commands, which is where most Payload-on-Cloudflare setups run into trouble. Running the verification steps at each stage, especially the explicit --remote queries against D1, confirms the resources are wired up correctly before the app ships to production.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

Further reading

  • Cloudflare API token creation
  • Wrangler installation
  • Cloudflare D1 Wrangler commands
  • Cloudflare R2 CLI
  • OpenNext Cloudflare bindings
  • Payload D1 adapter
  • Payload storage adapters