Deployment
OpenNext build pipeline, Cloudflare Worker bindings, environment variables, and domain onboarding.
CloudPos Ecom is deployed as a Cloudflare Worker using OpenNext. The build pipeline runs next build then opennextjs-cloudflare build, producing a cloudposcom-web Worker that handles SSR, ISR, and static assets. All configuration lives in wrangler.jsonc and open-next.config.ts.
Prerequisites
- Node.js 20+,
wranglerCLI (devDependency), Cloudflare account with Workers & Pages access. - R2 bucket created once before first deploy (see ISR Cache).
REVALIDATE_SECRETWorker secret set viawrangler secret put.
NPM Scripts
Script | Command | Use |
|---|---|---|
|
| Local development — plain Next.js, no Cloudflare emulation |
|
| Standalone Next.js build (CI check; not deployed directly) |
|
| Full OpenNext build + local Miniflare preview with all bindings |
|
| Build and push to Cloudflare Workers (production) |
|
| Build and upload without activating (staged deploy) |
|
| Regenerate TypeScript types for KV, R2, and Durable Object bindings |
|
| Verify every ThemeSlot in the active pack exports the required shape |
npm run dev runs against next dev without Cloudflare Workers emulation — KV lookups and Durable Objects are unavailable, so the tenant seed (src/config/tenants.js) is used instead of KV. Use npm run preview to test the full Cloudflare runtime locally.
Build Pipeline
npm run deploy
└─ opennextjs-cloudflare build
├─ next build (standard Next.js build)
└─ OpenNext adapter (wraps output for CF Workers)
→ .open-next/worker.js (Worker entry point)
→ .open-next/assets/ (static assets → Workers Assets binding)
└─ opennextjs-cloudflare deploy
└─ wrangler deploy (reads wrangler.jsonc)wrangler.jsonc sets "main": ".open-next/worker.js" and "assets": { "directory": ".open-next/assets" }. Static assets are served directly from the Workers Assets binding (ASSETS) without hitting the Worker.
wrangler.jsonc Overview
wrangler.jsonc is at the project root. Key settings:
{
"name": "cloudposcom-web",
"compatibility_date": "2026-05-15",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"workers_dev": true
}nodejs_compat enables Node.js built-ins in the Worker runtime. global_fetch_strictly_public prevents the Worker from fetching private Cloudflare IPs. workers_dev: true keeps the *.workers.dev URL active alongside custom domains for default-tenant smoke tests.
open-next.config.ts Overview
export default defineCloudflareConfig({
incrementalCache: withRegionalCache(r2IncrementalCache, {
mode: "long-lived",
shouldLazilyUpdateOnCacheHit: true,
}),
queue: doQueue,
tagCache: doShardedTagCache,
enableCacheInterception: true,
});r2IncrementalCache— stores ISR/SSG output in R2 so it persists across Worker isolate restarts.withRegionalCache— wraps R2 with a hot Cloudflare Cache API copy per colo. Hits are served from the regional cache; the copy is refreshed lazily on hit.doQueue— Durable Object queue that de-duplicates background ISR revalidation work.doShardedTagCache— Durable Object that backsrevalidateTag()/revalidatePath(). Without it, those calls are silent no-ops.enableCacheInterception: true— serves cached pages directly from the incremental cache, bypassing the full Next.js server handler on cache hits.
Bindings
KV Namespaces
Binding | KV Namespace ID | Purpose |
|---|---|---|
|
|
|
|
|
|
Middleware caches KV lookups in-isolate for 60 seconds to avoid per-request KV round-trips. The seed file (src/config/tenants.js) is the local/fallback source when KV is unavailable.
To push tenant config to KV:
# Push one tenant
node scripts/push-tenants.mjs --tenant mynewstore
# Push all tenants (from the seed file)
node scripts/push-tenants.mjs
# Dry run
node scripts/push-tenants.mjs --tenant mynewstore --dry-runR2 Bucket — ISR Cache
Binding | Bucket name |
|---|---|
|
|
Create the bucket once before deploying:
npx wrangler r2 bucket create cloudposcom-opennext-cacheWorkers Assets
Binding | Source directory |
|---|---|
|
|
Served directly by Cloudflare's edge — static files never hit the Worker.
Durable Objects
Binding | Class | Migration tag | Purpose |
|---|---|---|---|
|
|
| De-duplicates ISR background revalidations |
|
|
| Backs |
Migration tags are applied sequentially. v1 is already in production; v2 was added for the sharded tag cache class. Never remove earlier migration entries.
Images
"images": { "binding": "IMAGES" }The IMAGES binding wires up Cloudflare's native image transformation for next/image. Currently, next.config.mjs sets images: { unoptimized: true } as a temporary workaround for a transformation failure in the Worker. Remove unoptimized once Cloudflare Image Transformations are enabled on each tenant zone and the account has the Images entitlement.
Workers Service Binding (self-reference)
"services": [{ "binding": "WORKER_SELF_REFERENCE", "service": "cloudposcom-web" }]Required by OpenNext's cache interception path — the Worker calls itself via the service binding to serve cached responses.
Environment Variables
Set in wrangler.jsonc under vars (plain, visible in dashboard):
Variable | Example value | Purpose |
|---|---|---|
|
| Default ERP origin (fallback for single-tenant / build-time fetches) |
|
| Default |
|
| Canonical site URL for the default tenant |
Set secrets via wrangler secret put (never in wrangler.jsonc):
Secret | Purpose |
|---|---|
| Authenticates |
Security Headers
next.config.mjs sets static, tenant-independent headers on all routes:
Header | Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
The Content-Security-Policy is not set in next.config.mjs. It is built per-request in src/middleware.js from the resolved tenant's apiBaseUrl (pulled from KV), so onboarding a new client domain in KV needs no redeploy.
cf-typegen
After changing KV namespaces, R2 buckets, Durable Objects, or other bindings in wrangler.jsonc, regenerate the TypeScript interface:
npm run cf-typegen
# → writes cloudflare-env.d.tscloudflare-env.d.ts exposes the CloudflareEnv interface to TypeScript so middleware and route handlers get typed access to env.HOST_MAP, env.TENANTS, env.NEXT_INC_CACHE_R2_BUCKET, etc.
Adding a New Domain
- Add tenant to seed — add an entry in
src/config/tenants.jswithid,domains,apiBaseUrl,tenantKey,siteUrl,theme, and optionallyi18n. - Push to KV —
node scripts/push-tenants.mjs --tenant <id>. This writestenant:<id>to theTENANTSnamespace and each domain toHOST_MAP. No redeploy is needed — middleware picks up KV changes within 60 seconds. - Add the domain to the Worker — Cloudflare Dashboard → Workers & Pages →
cloudposcom-web→ Domains → Add Domain. Cloudflare auto-creates the CNAME in the zone (must be proxied). - Verify — visit the domain; DNS propagation on Cloudflare is typically under 1 minute.
Unknown hosts (not in KV or seed) are 307-redirected to https://cloudposai.com.
Preview vs. Deploy
Command | Effect |
|---|---|
| Builds and starts a local Miniflare instance. All bindings (KV, R2, DO) use local emulation. Use for end-to-end testing before pushing. |
| Builds and deploys to the live |
| Builds and uploads the Worker script without activating it. Useful for staged rollouts or inspecting the bundle before going live. |