A full Next.js + Payload app: one deployable unit that serves the public payloadcms.com experience (this repo: ceccec/website · upstream payloadcms/website).
Jump: What is delivered · Deploy · Manual · Vercel path · Cloudflare path · Runtime · Docker · Local
When you run this app in production, you get a single site with the following live surfaces (actual routes and feature flags depend on your env and data).
| Delivered | What the visitor or operator gets |
|---|---|
| Marketing site | Public pages (home, product, partners, case studies, pricing, etc.), dynamic routing, search integrations where configured, sitemap and OG metadata when env is set. |
| Documentation app | Docs UI backed by Payload content; sources can be synced from the payload repo, loaded by branch (/docs/dynamic/...), or from a local checkout (/docs/local/... + DOCS_DIR_V3). |
| Payload Admin | CMS at /admin — collections, globals, uploads, redirects, form builder, SEO fields, and admin UX included in this codebase. |
| APIs | Payload REST and GraphQL as enabled in config; Local API for server components and routes. |
| Payload Cloud (product UI) | In-app flows to manage Payload Cloud projects (GitHub connect, deployments, billing) when Cloud-related env and integrations are configured — Stripe and GraphQL for Cloud in this repo. |
| Media & cache | Uploads on Vercel Blob or Cloudflare R2 depending on stack; Workers builds also use R2 for OpenNext incremental cache per wrangler.jsonc. |
| Persistence | Content in Postgres or Cloudflare D1 (SQLite), selected automatically from env — see Runtime. Same product shape; not interchangeable DB files between the two engines. |
Stack: Next.js 15 (App Router), TypeScript, SCSS modules, Lexical / MDX flows for docs, light/dark UI without first-paint flicker on the marketing surfaces.
The buttons above point at ceccec/website; replace ceccec with payloadcms for upstream.
Deploy to Cloudflare (per Cloudflare documentation)
Only what that page states (see the page for full wording and examples):
- What the button does: clone the Git repository into the user’s GitHub or GitLab account; configure the project (repository name, Worker name, required resource names) on one setup page (changes reflected in the created repo); build and deploy with Workers Builds and deploy to the Cloudflare network — required resources are automatically provisioned and bound to the Worker without additional setup.
- Embedding: use the Markdown / HTML / URL snippets and replace the repository URL (optional subdirectory per that section). If you already use Workers Builds, you can copy a button snippet from the dashboard (share on the Worker).
- Automatic resource provisioning: Cloudflare reads the Wrangler configuration file in your repo to determine resource requirements, provisions resources, and updates the Wrangler configuration where applicable for newly created resources (for example database Ids and namespace Ids). Your repository must include default values for resource names, resource Ids and any other properties for each binding. Supported resource types are listed on that page.
- Worker environment variables and secrets: environment variables may be set in Wrangler as usual (
vars). Secrets may be listed in.dev.vars.exampleor.env.examplein dotenv format. Secrets Store bindings may be configured in Wrangler as in the doc’s examples. - Best practices: custom
buildanddeployscripts inpackage.jsonare automatically detected and pre-populated; users may change or accept them. If there is nodeployscript, Cloudflare preconfiguresnpx wrangler deploy. If there is nobuildscript, the build field is left blank. For D1 migrations run fromdeploy, the migration command should reference the binding name, not the database name. Optionalpackage.json→cloudflare→bindingsentries may include adescriptionper binding (including env vars/secrets); inline markdown is supported — see the doc’s example. - Limitations: monorepo caveats, subdirectory rules, one Deploy button per Workers app in a monorepo, Workers only (not Pages), github.com / gitlab.com only (no self-hosted), public repositories only.
- Workers build path:
scripts/build.mjs(oftenpnpm run workers:buildon CI). Deploy:package.jsonscripts (e.g.workers:deploy,deploy). See Runtime and DEPLOYMENT.md. - Binding descriptions for
cloudflare.bindings:config/cloudflare.bindings.json→pnpm sync:cloudflare-bindings→package.json. - Wrangler:
wrangler.jsonc. Env catalog (this codebase):config/cloudflare-env-reference.md.
Same deliverables as above once install and env match your host. Use ceccec/website below or change ORIGIN once.
From package.json engines:
node -v # expect >= 20.9.0
pnpm -v # expect >= 10.33.2Install dependencies once:
export ORIGIN=https://github.com/ceccec/website.git # or upstream: https://github.com/payloadcms/website.git
git clone "$ORIGIN" website && cd website
corepack enable pnpm # optional; ensures pnpm matches packageManager
pnpm iStack selection is automatic when Postgres URLs are set — see Runtime.
cp .env.example .envEdit .env (minimum for relational deploy — full list in .env.example):
# Paste into .env (replace placeholders)
POSTGRES_URL="postgresql://USER:PASS@HOST/DB?sslmode=require"
DATABASE_URL="$POSTGRES_URL"
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_..."
PAYLOAD_SECRET="$(openssl rand -hex 32)"
PAYLOAD_HOSTING=vercelDeploy build (matches typical Vercel Build Command):
pnpm buildOr configure Vercel Install Command pnpm i, Build Command pnpm build, add env vars in the dashboard. Guide: with-vercel-website.
For D1, avoid stray postgres://… in env (else Vercel stack wins), or set PAYLOAD_HOSTING=cloudflare. See Runtime.
cp .env.example .env
cp .dev.vars.example .dev.varsEdit .env / .dev.vars — at least PAYLOAD_SECRET (required for pnpm build / payload migrate in CI too — add the same value under Workers build env vars).
Create two D1 databases and paste each database_id into wrangler.jsonc (D1 get started):
pnpm exec wrangler d1 create payload-website
pnpm exec wrangler d1 create payload-website-next-tag-cached1_databases[0](binding:D1) — Payload CMS data (payload migrate).d1_databases[1](binding:NEXT_TAG_CACHE_D1) — OpenNext on-demand tag cache (OpenNext caching); SQL migrations live ind1-migrations/next-tag-cache/.
Durable Objects: OpenNext’s revalidation queue (NEXT_CACHE_DO_QUEUE / DOQueueHandler) is declared in wrangler.jsonc with a migrations entry — first deploy applies the DO migration.
Full Cloudflare deploy (migrate + OpenNext + deploy Worker):
pnpm run deployExplicit Workers pipeline (same as Cloudflare branch of pnpm build):
pnpm run workers:build # deploy:database + opennext:build
pnpm run workers:deploy # wrangler deploy via opennextDry-run config without publishing:
pnpm run deploy:dryReferences: DEPLOYMENT.md, DOCKER.md, with-cloudflare-d1.
Minimal vs full Cloudflare stack: Default wrangler.jsonc is enough to ship. Extra products (KV, Queues, Hyperdrive, Workers AI, Vectorize, …) are optional — merge snippets from config/wrangler.optional-bindings.jsonc only after you provision them (DEPLOYMENT.md).
Resolved in src/lib/deploymentTarget.ts (see src/payload.config.ts): Wrangler / Workers runtime → Cloudflare; else Vercel when VERCEL=1 or postgres://… is set; overrides via PAYLOAD_HOSTING.
| Stack | Typical triggers | DB | Files |
|---|---|---|---|
| Cloudflare | Worker runtime (navigator.userAgent includes Cloudflare-Workers), or no VERCEL / no Postgres URL (Node migrate toward D1), or PAYLOAD_HOSTING=cloudflare |
D1 | R2 + OpenNext cache |
| Vercel | VERCEL=1, or POSTGRES_URL / DATABASE_URL is postgres://…, or PAYLOAD_HOSTING=vercel (+ Postgres URL per config) |
Postgres | Vercel Blob |
pnpm exec payload migrateper production DB (migrations;src/migrations/index.ts). D1 and Postgres are separate schemas.- Workers CI: if Postgres vars leak in, set
PAYLOAD_HOSTING=cloudflareso migrate targets D1.
Templates: with-cloudflare-d1 · with-vercel-website
Scripts (copy one line)
pnpm build # routes via scripts/build.mjs → Vercel: next build | Workers CI: workers:build (OpenNext)
pnpm run build:vercel # `PAYLOAD_HOSTING=vercel` + same pipeline as `pnpm build` on the Node/Vercel/Docker stack
pnpm run deploy:database # migrate (+ remote D1 PRAGMA optimize on Cloudflare path)
pnpm run workers:build # deploy:database + opennext:build (also invoked by `pnpm build` on Workers CI)
pnpm run workers:deploy # deploy Worker after workers:build
pnpm run deploy # deploy:database + full Worker pipeline
pnpm run deploy:dry # dry-run Vercel + Cloudflare configs
pnpm sync:cloudflare-bindings # config/cloudflare.bindings.json → package.json `cloudflare.bindings`For a containerized Node stack — MongoDB, MinIO (S3), Mailpit (SMTP), Next + Payload on a Unix socket, and Nginx as the HTTP edge — see DOCKER.md and docker/CONVENTIONS.md (infrastructure = standard Docker images; no Vercel Blob / SendGrid required for core behavior). This path uses PAYLOAD_HOSTING=vercel; it does not replace Cloudflare Workers + D1 (Runtime).
Run the same app locally (deliverables depend on .env):
pnpm i
cp .env.example .env
pnpm devCloud UI expects https://local.payloadcms.com:3000:
# /etc/hosts (or Windows equivalent)
127.0.0.1 local.payloadcms.com
MIT.
