diff --git a/packages/twenty-website/next.config.ts b/packages/twenty-website/next.config.ts index 20b81c8f91..679df7d271 100644 --- a/packages/twenty-website/next.config.ts +++ b/packages/twenty-website/next.config.ts @@ -32,7 +32,14 @@ const SECURITY_HEADERS = [ { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" }, ] as const; +// Skew protection: CI sets DEPLOYMENT_ID at build time so it's baked into +// prerendered HTML + the RSC payloads. The Worker reads the same value at +// runtime (via the worker env var) and routes mismatched requests to the +// matching older Worker version via its preview URL. +const deploymentId = process.env.DEPLOYMENT_ID; + const nextConfig: LinariaConfig = { + deploymentId, images: { formats: ['image/avif', 'image/webp'], remotePatterns: [ diff --git a/packages/twenty-website/open-next.config.ts b/packages/twenty-website/open-next.config.ts index e4bc5eedc9..2a0ccd24dd 100644 --- a/packages/twenty-website/open-next.config.ts +++ b/packages/twenty-website/open-next.config.ts @@ -1,6 +1,21 @@ import { defineCloudflareConfig } from '@opennextjs/cloudflare'; import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache'; -export default defineCloudflareConfig({ +const baseConfig = defineCloudflareConfig({ incrementalCache: r2IncrementalCache, }); + +// `defineCloudflareConfig` only takes the `CloudflareOverrides` subset of the +// config today; `skewProtection` lives directly under `cloudflare.*` and has to +// be merged in. See packages/cloudflare/src/api/config.ts in opennextjs-cloudflare. +export default { + ...baseConfig, + cloudflare: { + ...baseConfig.cloudflare, + skewProtection: { + enabled: true, + maxNumberOfVersions: 10, + maxVersionAgeDays: 14, + }, + }, +}; diff --git a/packages/twenty-website/public/_headers b/packages/twenty-website/public/_headers deleted file mode 100644 index c846ea711e..0000000000 --- a/packages/twenty-website/public/_headers +++ /dev/null @@ -1,2 +0,0 @@ -/_next/static/* - Cache-Control: public, max-age=31536000, immutable diff --git a/packages/twenty-website/wrangler.jsonc b/packages/twenty-website/wrangler.jsonc index 968d4a994a..f2f0bd76b0 100644 --- a/packages/twenty-website/wrangler.jsonc +++ b/packages/twenty-website/wrangler.jsonc @@ -2,12 +2,24 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "twenty-website", "main": ".open-next/worker.js", - "compatibility_date": "2025-01-15", + "compatibility_date": "2026-04-15", + // `global_fetch_strictly_public` forces the skew handler's cross-version + // fetch to `-..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: `-..workers.dev`. + "preview_urls": true, "observability": { "enabled": true, }, @@ -30,6 +42,26 @@ "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 + // `-..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", @@ -49,6 +81,20 @@ "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", + }, }, }, }