00fad657f4
## Summary
The original goal of this whole migration: **cross-deployment skew is
now handled by OpenNext's per-version routing instead of by users having
to refresh.**
A client holding a stale tab from deployment X requests assets with
`?dpl=X` — the Worker compares X to the current `DEPLOYMENT_ID`, looks
it up in `CF_DEPLOYMENT_MAPPING`, and routes to the matching old Worker
version via its per-version preview URL
(`<old-version>-twenty-website-<env>.twentyhq.workers.dev`). The old
version serves the old assets / RSC payloads / Server Actions
consistently.
**Verified end-to-end on dev**:
| | Marker in HTML |
|---|---|
| Current Worker (`twenty-main.com/`) | `9npeiytir8EPOtW71cqDZ` |
| Stale request (`twenty-main.com/?dpl=<previous-deploy-id>`) |
`B9OC_TNl1vaGcJ5oUUty6` |
| Direct hit on old preview URL | `B9OC_TNl1vaGcJ5oUUty6` ← matches the
skew-routed response |
## Changes
**`open-next.config.ts`** — enable skew protection
```ts
const baseConfig = defineCloudflareConfig({ incrementalCache: r2IncrementalCache });
export default {
...baseConfig,
cloudflare: {
...baseConfig.cloudflare,
skewProtection: {
enabled: true,
maxNumberOfVersions: 10,
maxVersionAgeDays: 14,
},
},
};
```
(`defineCloudflareConfig` doesn't accept `skewProtection` directly — has
to be merged in)
**`next.config.ts`** — `deploymentId: process.env.DEPLOYMENT_ID`. CI
sets `DEPLOYMENT_ID` per-build; Next bakes it into prerendered HTML,
`?dpl=…` on asset URLs, Server Actions, and RSC fetch headers.
**`wrangler.jsonc`**:
- `compatibility_date: 2026-04-15` (was `2025-01-15`; build was warning)
- `assets.run_worker_first: true` — Worker must intercept asset requests
so the skew handler can route stale `/_next/static/*` to the old
version. CF edge cache absorbs hot paths so this isn't a 5×
billable-invocation tax
- `preview_urls: true` — required; skew routes via the per-version
preview URL which only exists when previews are enabled
- Per-env `services: [{ binding: WORKER_SELF_REFERENCE, service:
twenty-website-<env> }]` — OpenNext's recommended setup for
fire-and-forget ISR revalidation
- Per-env `vars`: `CF_WORKER_NAME` + `CF_PREVIEW_DOMAIN` (bare
`twentyhq`, *not* `twentyhq.workers.dev` — OpenNext appends
`.workers.dev` itself, see
[opennextjs-cloudflare#811](https://github.com/opennextjs/opennextjs-cloudflare/issues/811))
- Kept `global_fetch_strictly_public` in compat flags — without it, CF's
optimised intra-account routing self-loops the cross-version fetch and
522s out. With it, the fetch takes the public-Internet path which routes
correctly.
**`public/_headers`** — deleted (with `run_worker_first: true` the
assets pipeline doesn't process it; Next sets the same `Cache-Control:
immutable` on `/_next/static/*` anyway).
## Companion infra PR
https://github.com/twentyhq/twenty-infra/pull/__ — wires the four CF env
vars (`DEPLOYMENT_ID`, `CF_WORKER_NAME`, `CF_PREVIEW_DOMAIN`,
`CF_ACCOUNT_ID`, `CF_WORKERS_SCRIPTS_API_TOKEN`) into the deploy
workflow.
## Known limitation
Skew routing only works for Worker versions deployed AFTER this PR
(older versions don't have `preview_urls: true` and don't have
`DEPLOYMENT_ID` bindings OpenNext can read). Users on tabs older than
the first post-merge deploy still fall through to the current Worker
(same behaviour as today).
OpenNext marks `skewProtection` as **experimental** in their type docs
("might break on minor releases") — worth keeping an eye on.
101 lines
3.5 KiB
JSON
101 lines
3.5 KiB
JSON
{
|
||
"$schema": "node_modules/wrangler/config-schema.json",
|
||
"name": "twenty-website",
|
||
"main": ".open-next/worker.js",
|
||
"compatibility_date": "2026-04-15",
|
||
// `global_fetch_strictly_public` forces the skew handler's cross-version
|
||
// fetch to `<version>-<worker>.<account>.workers.dev` to take the public
|
||
// Internet path. Without it, Cloudflare's optimized intra-account routing
|
||
// self-loops back to the current worker (timeouts → 522).
|
||
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||
"assets": {
|
||
"directory": ".open-next/assets",
|
||
"binding": "ASSETS",
|
||
// Worker must intercept asset requests so it can route /_next/static/*
|
||
// and /_next/data/* from a stale client to the matching old Worker
|
||
// version (skew protection). Cloudflare's edge cache still absorbs hot
|
||
// paths, so this isn't a 5× Worker invocation tax in practice.
|
||
"run_worker_first": true,
|
||
},
|
||
// Per-version preview URLs are how skew protection routes a stale request
|
||
// to the old deployment: `<version>-<worker>.<account>.workers.dev`.
|
||
"preview_urls": true,
|
||
"observability": {
|
||
"enabled": true,
|
||
},
|
||
"env": {
|
||
"dev": {
|
||
"name": "twenty-website-dev",
|
||
"routes": [
|
||
{
|
||
"pattern": "twenty-main.com",
|
||
"custom_domain": true,
|
||
},
|
||
{
|
||
"pattern": "www.twenty-main.com",
|
||
"custom_domain": true,
|
||
},
|
||
],
|
||
"r2_buckets": [
|
||
{
|
||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||
"bucket_name": "twenty-website-cache-dev",
|
||
},
|
||
],
|
||
// Self-reference so OpenNext can fire-and-forget background ISR
|
||
// revalidations instead of blocking the request. Per-env because the
|
||
// service name must match the deployed worker name.
|
||
"services": [
|
||
{
|
||
"binding": "WORKER_SELF_REFERENCE",
|
||
"service": "twenty-website-dev",
|
||
},
|
||
],
|
||
// Skew-protection runtime inputs. The handler reads these to construct
|
||
// `<old-version-id>-<CF_WORKER_NAME>.<CF_PREVIEW_DOMAIN>.workers.dev`
|
||
// when routing a stale request to an older Worker version.
|
||
"vars": {
|
||
"CF_WORKER_NAME": "twenty-website-dev",
|
||
// OpenNext appends `.workers.dev` itself when constructing the
|
||
// per-version preview URL — passing the full `twentyhq.workers.dev`
|
||
// here would yield `…twentyhq.workers.dev.workers.dev` and 404.
|
||
// See https://github.com/opennextjs/opennextjs-cloudflare/issues/811
|
||
"CF_PREVIEW_DOMAIN": "twentyhq",
|
||
},
|
||
},
|
||
"prod": {
|
||
"name": "twenty-website-prod",
|
||
"routes": [
|
||
{
|
||
"pattern": "twenty.com",
|
||
"custom_domain": true,
|
||
},
|
||
{
|
||
"pattern": "www.twenty.com",
|
||
"custom_domain": true,
|
||
},
|
||
],
|
||
"r2_buckets": [
|
||
{
|
||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||
"bucket_name": "twenty-website-cache-prod",
|
||
},
|
||
],
|
||
"services": [
|
||
{
|
||
"binding": "WORKER_SELF_REFERENCE",
|
||
"service": "twenty-website-prod",
|
||
},
|
||
],
|
||
"vars": {
|
||
"CF_WORKER_NAME": "twenty-website-prod",
|
||
// OpenNext appends `.workers.dev` itself when constructing the
|
||
// per-version preview URL — passing the full `twentyhq.workers.dev`
|
||
// here would yield `…twentyhq.workers.dev.workers.dev` and 404.
|
||
// See https://github.com/opennextjs/opennextjs-cloudflare/issues/811
|
||
"CF_PREVIEW_DOMAIN": "twentyhq",
|
||
},
|
||
},
|
||
},
|
||
}
|