Files
twenty/packages/twenty-website/wrangler.jsonc
T
Félix Malfait 00fad657f4 feat(website): enable OpenNext skew protection + tune CF cache (#20760)
## 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.
2026-05-20 12:47:12 +02:00

101 lines
3.5 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"$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",
},
},
},
}