The website's Cloudflare build (`opennextjs-cloudflare` / Turbopack) was
failing with `_defineProperty is not a function` while linaria/wyw
evaluates `twenty-ui/dist/theme.cjs` at build time. It regressed in
#21946, whose twenty-ui build rework changed the emitted `theme.cjs` so
the theme objects ship as runtime object spreads (`{ ...THEME_COMMON
}`).
**Cause:** wyw evaluates modules in Node through `next/babel`, which
pulls in `preset-env` + `transform-runtime`. Those re-lower the runtime
spreads into `@babel/runtime` helpers imported as ESM; wyw then
`require()`s that ESM module in a CJS context where the export is not
callable, so `_defineProperty` fails.
**Fix:** wyw runs in Node and needs no downleveling, so replace
`next/babel` with minimal presets (`@babel/preset-typescript`,
`@babel/preset-react`, `@wyw-in-js/babel-preset`, plus
`@babel/plugin-transform-export-namespace-from`), matching
twenty-front's wyw config. No `@babel/runtime` helpers get injected.
Kept on the website side so twenty-ui keeps react/react-dom as peer deps
(#21946).
Note: no blocking PR check runs the website production build, so this is
best validated via the website preview build or the twenty-infra deploy.
Renaming the package so any further PRs directed to the website are
targeted to the reworked code instead of diverging. Once merged, I will
start preparing this for deployment to dev to test before releasing to
prod. Any improvements will also be applied to this package.
I avoided making significant changes to API routes so nothing breaks,
but will test it thoroughly today to confirm. That said, everything is
ported - double checked.
Big diff PR, impossible to review, but last one! No more rebuilds.
## Summary (twenty-website — partner application form)
- **Required fields**: website URL, city, hourly rate, minimum project
are now required (client step-gate + server zod) with `*` markers. The
final step validates before POSTing, so empty required fields fail fast
client-side instead of round-tripping.
- **Technical skills reworked to *complement* "What you cover"** (the
service categories) rather than duplicate them — now a small shown set +
a larger searchable-only pool of tools / technologies / industries.
Field hint clarifies the intent.
- **No competitor CRMs** in suggestions (Salesforce/HubSpot/Attio
removed); a guard test fails if one ever reappears. Migrations surface
as a generic "CRM migration".
- `Form.TagInput` gains an optional `searchPool` prop (autocomplete-only
entries, not rendered as chips).
Companion to the app-side PR #21709.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21710?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
Fixes the **high-severity** `serialize-javascript` RCE advisory
(RegExp.flags / Date.prototype.toISOString, patched in **7.0.5**).
- Bumps the direct dep in `twenty-website` `^6.0.2 → ^7.0.5`.
- Only consumer is `src/lib/seo/JsonLd.tsx` (default-export API,
unchanged in v7 — the major only drops old Node support).
- `twenty-website` typecheck passes; lockfile regenerated under hardened
mode (`--immutable --check-cache` clean).
Our scanner flags the `dangerouslySetInnerHTML` in `JsonLd.tsx` as a
potential XSS sink. Since JSON-LD must be emitted as raw `<script
type="application/ld+json">` text (rendering it as a React child
HTML-entity-escapes it and corrupts the JSON, and the site is statically
generated so it must be in the SSG HTML for crawlers),
`dangerouslySetInnerHTML` is the correct, Next.js-documented approach
(the real fix is sanitizing the payload). This PR swaps our hand-rolled
`JSON.stringify().replace(/</g, ...)` for
[`serialize-javascript`](https://www.npmjs.com/package/serialize-javascript)
in `isJSON` mode, the maintained library [Next.js explicitly
recommends](https://nextjs.org/docs/app/guides/json-ld) for this, so the
script-unsafe characters are escaped by a vetted serializer rather than
custom code.
## What
Rebinds the marketplace's expertise facet from `deploymentExpertise`
(Cloud / Self-host) to **`partnerScope`** — the five partner Categories:
Advisory & Discovery · Solutioning · Custom Development · Hosting &
Infrastructure · Training & Adoption.
Moves the card chip, the profile facts row, the dropdown filter, the
`?categories=` URL param, and the API-boundary normalization onto
`partnerScope`. The standalone Cloud/Self-host facet is **dropped**
(hosting is now the `HOSTING` category), per the harmonization decision.
## Depends on
- The app exposing `partnerScope` — companion app PR #21126.
- The new `partnerScope` options + data migration — signup app PR
#21040.
## Tests
TDD red→green on: `filter-partners`, both API normalizers,
`filter-url-helpers`, `PartnerCard`, `use-filter-state`. 53/53 pass;
typecheck + lint + format clean.
## Merge order (we'll decide)
Independent diff. Suggested last of the four, after the signup PRs
(#21039 / #21040) and the app PR (#21126). Run `lingui:extract` once
after #21039 merges so the `.po` files don't conflict twice. Deploy the
app + migrate before the website ships.
## Summary
Follow-up to #20966 (Stripe fetch client). axios's default Node http
adapter on workerd has the same TLS hang the Stripe SDK had — it just
doesn't surface today because all three call sites are wrapped in
\`unstable_cache(revalidate: 3600)\` and the cache is populated at build
time, so misses are rare and the failure mode is a silent \`null\` to
the layout.
This swaps the three remaining axios calls in \`twenty-website\` for
native \`fetch\` and removes axios from
\`packages/twenty-website/package.json\`. The package is still used by
other workspaces, so yarn.lock keeps the other resolution.
Touched call sites:
- \`src/lib/releases/fetch-latest-release-tag.ts\` (GitHub releases —
runs at build time, cosmetic)
- \`src/lib/community/fetch-github-star-count.ts\` (GitHub star count in
menu)
- \`src/lib/community/fetch-discord-member-count.ts\` (Discord member
count in menu)
## Test plan
- [ ] After merge + deploy: confirm GitHub star + Discord member counts
render in the site menu (non-zero, formatted)
- [ ] Confirm \`/releases\` shows the latest tag-gated visible release
notes
- [ ] No \`axios\` in worker bundle (\`grep axios .open-next/worker.js\`
should be empty)
Removed the releases page’s runtime dependency on `fs` and
`process.cwd()` by introducing a build-time manifest generator: release
notes still live as markdown under `src/content/releases`, but a new
script now parses their frontmatter/content, validates that each note
has a release, title, and preview image (and that the image actually
exists), sorts the notes, and emits a typed `generated-release-notes.ts`
file that the app imports at runtime.
Updated the releases loader to return that generated data, changed the
menu releases preview and release JSON-LD to use explicit typed fields
(`title`, `previewImage`) instead of scraping markdown with regex at
runtime, wired the generator into Nx so it runs automatically before
`dev`, `build`, and `typecheck`, and fixed two stale image references in
the release MDX files that the new validation exposed.
---------
Co-authored-by: prastoin <paul@twenty.com>
## Summary
- Set `twenty-website`'s `dev` script to run Next.js on port `3002` by
default.
- Set `twenty-website`'s `start` script to use the same default port.
## Why
`twenty-website` previously inherited Next.js' default port `3000`,
which is also Twenty's backend/server default. The main Twenty frontend
already defaults to `3001`, so using `3002` for the website avoids local
port collisions when running the website next to the app server and
frontend.
This keeps the local convention sequential:
- `3000`: Twenty backend/server
- `3001`: Twenty frontend app
- `3002`: Twenty website
## Validation
- Parsed `packages/twenty-website/package.json` successfully with Node.
- Ran `git diff --check` for the changed package file.
- Verified Next.js supports the `--port` option for `next dev`.
---------
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
## Summary
Follow-up to the Cloudflare/OpenNext migration (#20741). Now that the
legacy `twenty-website` package was already removed in #20270, the
`-new` suffix on the marketing site package is no longer meaningful.
## What changes
- **Directory rename**: `git mv packages/twenty-website-new
packages/twenty-website` (1213 files moved, no content change)
- **Package + nx config**: `package.json` and `project.json` name fields
updated, `sourceRoot` repointed
- **Source refs**: `load-local-articles.ts` and
`load-local-release-notes.ts` had a hardcoded `'twenty-website-new'`
segment in their monorepo-root fallback path;
`app/[locale]/releases/page.tsx` had display strings showing where to
add content
- **External refs**: root `package.json` workspaces, root `CLAUDE.md` /
`README.md`, `twenty-sdk` + `create-twenty-app` READMEs,
`.vscode/twenty.code-workspace`, `.cursor/rules/changelog-process.mdc`,
Crowdin config + the three `website-i18n-*` CI workflows +
`ci-website.yaml`
- **Docker cleanup**:
`packages/twenty-docker/twenty-website-new/Dockerfile` deleted; the two
Makefile targets (`prod-website-new-build` / `prod-website-new-run`)
that referenced it removed — EKS deploy was retired in the Cloudflare
migration
- **`yarn.lock`** regenerated against the new workspace path
## What's deliberately not in this PR
The dev hostname `website-new.twenty-main.com` in `wrangler.jsonc` stays
for now. Migrating it to `website.twenty-main.com` needs coordinated DNS
deletion (current CNAME points at the legacy Docusaurus NLB and serves
503s) and removal of the matching legacy `website` Helm chart in
`twenty-infra`. Flagged as a separate cleanup.
Companion infra PR: https://github.com/twentyhq/twenty-infra/pull/682
(workflow paths + Terraform ECR + docs)
## Test plan
- [x] `yarn install --immutable` resolves clean against the new path
- [x] `npx nx typecheck twenty-website` passes
- [x] `npx nx lint twenty-website` passes
- [ ] CI on this PR confirms the same on a fresh checkout
- [ ] After merge: trigger `Deploy Website` workflow against
`environment=dev` to confirm the renamed working-directory deploys
correctly
## Summary
Implements enterprise licensing and per-seat billing for self-hosted
environments, with Stripe as the single source of truth for subscription
data.
### Components
- **twenty-website** hosts the private key to sign `ENTERPRISE_KEY` and
`ENTERPRISE_VALIDITY_TOKEN`. It communicates with Stripe to emit the
daily `ENTERPRISE_VALIDITY_TOKEN` if the subscription is active, based
on the user's Stripe subscription ID stored in `ENTERPRISE_KEY`.
- **Stripe** is the single source of truth for subscription data
(status, seats, billing).
- **The client** (twenty-server + DB + workers) saves `ENTERPRISE_KEY`
in the `keyValuePair` table (or `.env` if
`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false) and the daily-renewed
`ENTERPRISE_VALIDITY_TOKEN` in the `appToken` table.
`ENTERPRISE_VALIDITY_TOKEN` is verified client-side using a public key
to grant access to enterprise features (RLS, SSO, audit logs, etc.).
### Flow
1. When requesting an upgrade to an enterprise plan (from **Enterprise**
in settings), the user is shown a modal to choose monthly/yearly
billing, then redirected to Stripe to enter payment details. After
checkout, they land on twenty-website where they are exposed to their
`ENTERPRISE_KEY`, which they paste in the UI. It is saved in the
`keyValuePair` table. On activation, a first `ENTERPRISE_VALIDITY_TOKEN`
with 30-day validity is stored in the `appToken` table.
2. **Every day**, a cron job runs and does two things:
- **Refreshes the validity token**: communicates with twenty-website to
get a new `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity if the Stripe
subscription is still active. If the subscription is in cancellation,
the emitted token has a validity equal to the cancellation date. If it's
no longer valid, the token is not replaced. The cron only needs to run
every 30 days in practice, but runs daily so it's resilient to
occasional failures.
- **Reports seat count**: counts active (non-soft-deleted)
`UserWorkspace` entries and sends the count to twenty-website, which
updates the Stripe subscription quantity with proration. Seats are also
reported on first activation. If the subscription is canceled or
scheduled for cancellation, the seat update is skipped.
3. `ENTERPRISE_VALIDITY_TOKEN` is verified server-side via a public key
to grant access to enterprise features.
### Key concepts
Three distinct checks are exposed as GraphQL fields on `Workspace`:
| Field | Meaning |
|---|---|
| `hasValidEnterpriseKey` | Has any valid enterprise key (signed JWT
**or** legacy plain string) |
| `hasValidSignedEnterpriseKey` | `ENTERPRISE_KEY` is a properly signed
JWT (billing portal makes sense) |
| `hasValidEnterpriseValidityToken` | `ENTERPRISE_VALIDITY_TOKEN` is
present and not expired (expiration depends on signed token payload, not
on "expiresAt" on appToken table which is only indicative) |
Feature access is gated by `isValid()` =
`hasValidEnterpriseValidityToken || hasValidEnterpriseKey` (to support
both new and legacy keys during transition). After transition isValid()
= hasValidEnterpriseValidityToken
### Frontend states
The Enterprise settings page handles multiple states:
- **No key**: show "Get Enterprise" with checkout modal
- **Orphaned validity token** (token valid but no signed key): prompt
user to set a valid enterprise key
- **Active/trialing but no validity token**: show subscription status
with a "Reload validity token" action
- **Active/trialing**: show full subscription info, billing portal
access, cancel option
- **Cancellation scheduled**: show cancellation date, billing portal
- **Canceled**: show billing history link and option to start a new
subscription
- **Past due / Incomplete**: prompt to update payment or restart
### Temporary retro-compatibility: legacy plain-text keys
Previously, enterprise features were gated by a simple check: any
non-empty string in `ENTERPRISE_KEY` granted access. With this PR, we
transition to a controlled system relying on signed JWTs.
To avoid breaking existing self-hosted users:
- **Legacy plain-text keys still grant access** to enterprise features.
`hasValidEnterpriseKey` returns `true` for both signed JWTs and plain
strings, and `isValid()` checks `hasValidEnterpriseKey` as a fallback
when no validity token is present.
- **A deprecation banner** is shown at the top of the app when
`hasValidEnterpriseKey` is `true` but `hasValidSignedEnterpriseKey` is
`false`, informing the user that their key format is deprecated and they
should activate a new signed key.
- **No billing portal or subscription management** is available for
legacy keys since there is no Stripe subscription to manage.
This retro-compatibility will be removed in a future version. At that
point, `isValid()` will only check `hasValidEnterpriseValidityToken`.
### Edge cases
- **Air-gapped / production environments**: for self-hosted clients that
block external traffic (or for our own production), provide a long-lived
`ENTERPRISE_VALIDITY_TOKEN` (e.g. 99 years) directly in the `appToken`
table, with no `ENTERPRISE_KEY`. The daily cron will skip the refresh
(no enterprise key to authenticate with), but the pre-seeded validity
token will be used to grant feature access. No billing or seat reporting
occurs in this mode.
- **`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false**: if the user tries to
activate an enterprise key but DB config writes are disabled, the
backend returns a clear error asking them to add `ENTERPRISE_KEY` to
their `.env` file manually.
- **Canceled subscriptions**: the `/seats` endpoint skips Stripe updates
for canceled or cancellation-scheduled subscriptions to avoid Stripe API
errors.
### How to test
- launch twenty-website on a different url (eg localhost:1002)
- add ENTERPRISE_API_URL=http://localhost:3002/api/enterprise (or else)
in your server .env
- ask me for twenty-website's .env file content (STRIPE_SECRET_KEY;
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID;STRIPE_ENTERPRISE_YEARLY_PRICE_ID;
ENTERPRISE_JWT_PRIVATE_KEY; ENTERPRISE_JWT_PUBLIC_KEY;
NEXT_PUBLIC_WEBSITE_URL)
- visit Admin panel / enterprise
Updated both drizzle-kit and drizzle-orm to the latest versions.
Process:
- Ran migrations on the database.
- Updated the versions.
- Made the required changes to `drizzle-posgres.config.ts`.
- Ensured migrations are not out of sync be running them again - no
issues, no updates applied.
- Updated the snapshots.
- Deleted the database named `website`.
- Re-ran the migrations to confirm.
There are no breaking changes because the surface drizzle-orm covers is
limited. However, we had to update drizzle-orm in order to update
drizzle-kit to a version greater than 0.27.0 in order to avoid the use
of hono. Therefore, I went ahead and updated both to the latest.
Resolves [Dependabot Alert
#274](https://github.com/twentyhq/twenty/security/dependabot/274) and
three others.
Standardized `PageProps` usage across multiple pages. Upgraded `next`,
`eslint-config-next`, and related dependencies to `^15.5.4` for
compatibility and performance improvements.
- Move dev-only types to devDependencies
- Move frontend-only deps from root to twenty-front
- Add website-only deps to twenty-website
- Fix react-phone-number-input patch path
CI should validate.
Now the source of truth for the version is set during the build process.
We set it as an environment variable from the tags.
We could add it back to the package.json during the build process (from
the git tag), but there is not use for it at the moment since it's not
npm packages.
# Introduction
closes https://github.com/twentyhq/core-team-issues/issues/591
Same than for `twenty-shared` made in
https://github.com/twentyhq/twenty/pull/11083.
## TODO
- [x] Manual migrate twenty-website twenty-ui imports
## What's next:
- Generate barrel and migration script factorization within own package
+ tests
- Refactoring using preconstruct ? TimeBox
- Lint circular dependencies
- Lint import from barrel and forbid them
### Preconstruct
We need custom rollup plugins addition, but preconstruct does not expose
its rollup configuration. It might be possible to handle this using the
babel overrides. But was a big tunnel.
We could give it a try afterwards ! ( allowing cjs interop and stuff
like that )
Stuck to vite lib app
Closed related PRs:
- https://github.com/twentyhq/twenty/pull/11294
- https://github.com/twentyhq/twenty/pull/11203
This PR introduces Keystatic to let us edit twenty.com's content with a
CMS. For now, we'll focus on creating release notes through Keystatic as
it uses quite simple Markdown. Other types of content will need some
refactoring to work with Keystatic.
https://github.com/user-attachments/assets/e9f85bbf-daff-4b41-bc97-d1baf63758b2
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
Fix all the broken CIs :p
This includes an ongoing effort to simplify test maintenance by having 1
unique source of truth about metadata and data mocks (that will later be
generated from a unique source of seeds: dev = demo = test)
Regressions:
- Unit line coverage: 60 > 55
- Storybook Pages branch coverage: 40 > 35
We will need to write tests to increase those coverage
- RelationFieldDisplay perf: 0.2ms to 0.22ms > We might have a
regression here
- Removed perf story about RawJSON > We will need to re-add it
We are updating our git worklow.
Case 1: **URGENT / PATCH**
If you want to include something URGENT that cannot wait for the next
release, you'll need to:
- create a PR from the latest patch (right now v0.30.1)
- create a new patch tag from this PR (would be v0.30.2 right now)
- merge this PR in main so it's in 0.31 too
Case 2: **REGULAR**
- Open a PR from main and merge it into main
I'm tagging main as v0.31.canary to make it clear!
In this PR:
- update your environment variables to default `CACHE_STORAGE_TYPE` to
`redis` and `MESSAGE_QUEUE_TYPE` to `bull-mq`
- add redis container to our default docker-compose
- add `REDIS_HOST` and `REDIS_PORT` to docker-compose yaml
- add upgrade instructions