Add `export const dynamic = 'force-static'` to the releases page to
prevent runtime re-renders on Cloudflare Workers where fs is
unavailable, which caused stale "Releases were not found" errors after
ISR cache eviction.
Also moves the "Up to 5 workspaces" bullet from the Organization plan to
the Pro plan on the self-hosting pricing view.
## What this PR does
Adds the **Partners Marketplace** page to the Twenty marketing website
(`/partners-marketplace`), built with Next.js App Router. The page
fetches live partner data from the Twenty API and presents it in a
responsive grid with an interactive filter bar.
## Partners grid
- Fetches partners from the `/s/partners` endpoint via a typed
`getPartners()` server-side fetcher
- Responsive 1 → 2 → 3 column grid (mobile / tablet / desktop)
- Each card shows name, region eyebrow, intro text, chip rows (Regions /
Languages / Deploys), and a Calendly CTA
- Stagger entrance animation (700ms cascade, respects
`prefers-reduced-motion`)
## Filter bar
- Three facets: **Region**, **Language**, **Deployment** — multi-select
chips
- **Selection model:** OR within a facet, AND across facets (e.g.
`Europe OR US` AND `French`)
- Filter state lives in URL search params
(`?regions=EUROPE,US&languages=FRENCH`) — filtered views are shareable
and browser-back works correctly
- Client-side filtering — no server round-trip per interaction
- Result count ("Showing 3 of 8 partners") updates live with
`aria-live="polite"`
- "Clear filters" button resets all facets in one URL update, only shown
when filters are active
- Empty state ("No partners match your filters") replaces the grid when
nothing matches
- 200ms opacity fade-out on card removal; initial stagger animation
preserved on first load
- `prefers-reduced-motion: reduce` disables all transitions
## Architecture
- `page.tsx` stays a **Server Component** — fetches partners
server-side, all partner HTML is in the initial response for SEO
- `<MarketplaceClient>` is the client boundary — owns filter state via
`useFilterState()` (backed by `useSearchParams`)
- Canonical URL set in page metadata so `?regions=...` deep-links don't
get indexed as duplicates
- `<Suspense>` wrapper around `MarketplaceClient` for Next.js 15
`useSearchParams` compliance
- No new npm dependencies
## Test coverage
31 tests across three suites:
- `filter-partners.test.ts` — pure filter logic (OR / AND semantics,
empty results)
- `filter-url-helpers.test.ts` — URL param encode / decode / toggle /
round-trip
- `use-filter-state.test.tsx` — hook behaviour with mocked
`next/navigation`
## Screenshot
<img width="1783" height="1196" alt="Screenshot 2026-05-17 at 15 01 54"
src="https://github.com/user-attachments/assets/9dddf827-f440-4cad-8ec3-81ede6d46434"
/>
## Test plan
- [ ] Navigate to `/partners-marketplace` — all live partners render
- [ ] Click a Region chip — URL updates with `?regions=...`, cards
filter, count updates
- [ ] Click the same chip again — selection removed, all cards return
- [ ] Select chips from two different facets — AND behaviour narrows
results correctly
- [ ] Trigger empty state (e.g. filter to a region with no partners) —
empty state shown with "Clear filters" button
- [ ] Click "Clear filters" — all cards return, URL cleared
- [ ] Deep-link to `?regions=EUROPE&languages=FRENCH` — page loads with
filters applied
- [ ] Browser back button restores previous filter state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Customer story page shows the following error, which I believe leads to
an internal server on the individual customer story pages.
<img width="636" height="75" alt="image"
src="https://github.com/user-attachments/assets/fc9ede75-fd3b-4538-8211-8182f4a99b9b"
/>
This PR replaces remote URLs of those images with local copies to avoid
a 404 issue. Will test once deployed on dev to confirm if the error is
resolved, but locally, I do not see console errors any longer after this
change.
There is some duplicated copy that I found upon audit which can be made
DRY, but I will resolve it in a separate PR to keep this PR
single-responsibility.
## 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
Adds review apps for the marketing site. Every PR that touches
`packages/twenty-website/**` or `packages/twenty-shared/**` gets a
per-version Worker preview URL, sticky-commented on the PR, auto-cleaned
up when the PR closes.
Same Cloudflare machinery skew protection rides on, just used for
previews — no extra plan, no extra services. Cleaner than the
GitHub-Actions-runner + Cloudflare-tunnel pattern: previews persist for
the life of the version, accessible from anywhere, no warm-up.
## Files
- **`.github/workflows/website-pr-preview.yaml`** — on PR
open/sync/reopen: builds the Worker with a per-PR `DEPLOYMENT_ID`, runs
`wrangler versions upload --tag pr-<N>` (no production traffic),
sticky-comments the preview URL. Skipped on fork PRs because GitHub
doesn't pass secrets to forks anyway.
- **`.github/workflows/website-pr-preview-cleanup.yaml`** — on PR close:
walks the Worker version list via the CF API, deletes anything tagged
`pr-<N>` (with message-based fallback if the annotation key changes),
updates the sticky comment.
- **`open-next.config.ts`** — `maxNumberOfVersions: 10 → 50` to leave
room for PR previews on top of skew protection's prod-version retention.
## How it looks on a PR
The bot leaves a sticky comment like:
> 🔍 **Website preview** is up at
**https://abc12345-twenty-website-dev.twentyhq.workers.dev**
>
> | | |
> |---|---|
> | Version | `abc12345-...` |
> | Commit | `<sha>` |
> | Bindings | shared with the `dev` Worker (R2 cache + secrets) |
>
> Updates on every push. Auto-deleted when the PR closes.
On close it becomes:
> 🧹 Website preview for this PR was cleaned up after close.
## Twenty repo credentials already provisioned
- `secret CLOUDFLARE_API_TOKEN` — same scoped token the `twenty-infra`
workflow uses
- `var CLOUDFLARE_ACCOUNT_ID` = `67b2bbe4381006564d2b0aa6ce6177be`
- `var CF_PREVIEW_DOMAIN` = `twentyhq` (no `.workers.dev` suffix —
OpenNext appends it;
[opennextjs-cloudflare#811](https://github.com/opennextjs/opennextjs-cloudflare/issues/811))
## Known limitations
- **Shared dev bindings**: PR previews use the dev Worker's R2 bucket +
secrets (Stripe test key, JWT private key). Fine for a read-mostly
marketing site; if two simultaneous PRs ever fight over ISR cache state
we can prefix R2 keys per-PR later.
- **Fork PRs don't get previews**. GitHub Actions doesn't pass
`secrets.*` to fork-PR runs (security), and the wrangler upload requires
the CF token. To enable forks, would need to switch to
`pull_request_target` and gate on a maintainer label — not done here
because the security tradeoff isn't worth it for a marketing-site
preview.
- **Version cap**: 50 versions is the new ceiling, and
`maxVersionAgeDays: 14` auto-prunes anything older. Cleanup-on-close
should keep us well under in steady state.
## Test plan
- [ ] CI on this PR triggers the preview workflow itself; check that the
sticky comment appears with a working URL
- [ ] Hit the URL, click around — should look like a fresh
marketing-site build with this PR's changes
- [ ] Close (don't merge) → cleanup workflow should run; sticky comment
switches to the "cleaned up" message; the version is gone from `wrangler
versions list --name twenty-website-dev`
## 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.
## Summary
Drops the `website.` subdomain on dev entirely and serves the marketing
site from the bare zone + `www`, mirroring how prod is served at
`twenty.com` + `www.twenty.com`.
Also fixes a latent root-path substitution bug in the existing www→apex
redirect that was masked on prod by a CF-level redirect.
## What changes
- `wrangler.jsonc` env.dev routes: `twenty-main.com` (apex) +
`www.twenty-main.com` (was `website.twenty-main.com`)
- `next.config.ts`: extends host-based www→apex redirect to also cover
`www.twenty-main.com`, and adds explicit `source: '/'` rules for both
prod + dev before the catch-all `source: '/:path*'` (the `:path*`
parameter doesn't substitute properly when it matches the empty root
path against an absolute destination URL — Next.js leaves the literal
`:path*` in the `Location` header)
## Live verification (after redeploy)
| URL | Status | Notes |
|---|---|---|
| `https://twenty-main.com/` | 200 | `x-opennext: 1`, `x-nextjs-cache:
HIT` |
| `https://twenty-main.com/pricing` | 200 | Worker SSR |
| `https://www.twenty-main.com/` | 308 → `https://twenty-main.com/` |
Root-redirect fix applied |
| `https://www.twenty-main.com/pricing` | 308 →
`https://twenty-main.com/pricing` | Path preserved |
| `https://twenty.com/` | 200 | Unchanged |
| `https://www.twenty.com/` | 301 → `https://twenty.com/` | Still routed
via CF-level redirect, now also covered by the new explicit Next rule as
a defense-in-depth |
| `https://website.twenty-main.com/` | 503 | DNS record removed by
wrangler when route was deleted; hostname effectively retired |
## Companion infra PR
https://github.com/twentyhq/twenty-infra/pull/__ —
`cloudflare/website/dev.env` + `docs/4-environments.md` updated to the
new URL; also bundles the CI fix that should have landed in #683 (was
pushed too late).
## Summary
Last "-new" trace in the source repo, follow-up to the rename in #20745.
The dev Worker custom domain swaps from `website-new.twenty-main.com` to
`website.twenty-main.com`, matching the prod pattern (no "-new"
anywhere).
## Live operations already performed
- Deleted the legacy CNAME at `website.twenty-main.com` that pointed at
the dev EKS NLB (record id `52b4a4174dfd382ecf38111b7f08e642`, was the
Docusaurus dev deploy that had been 503'ing)
- Redeployed `twenty-website-dev` Worker — Wrangler provisioned the new
custom domain via the CF API
- Verified `https://website.twenty-main.com/` returns 200 with
`x-opennext: 1` and `x-nextjs-cache: HIT`
The old hostname `website-new.twenty-main.com` is now unbound; Wrangler
removed its DNS record when the route disappeared from this file.
Visitors get 522, which is the desired state for a retired hostname.
## Companion infra PR
https://github.com/twentyhq/twenty-infra/pull/683 — removes
`charts/dev/apps/website` Helm chart (the legacy Docusaurus deploy) +
ArgoCD app, updates `cloudflare/website/dev.env` and
`docs/4-environments.md`.
## Out of scope
- Legacy `website.twenty-staging.com` and `website.twenty.com` are still
alive serving Docusaurus content. Decommissioning those is a separate
decision (those URLs may still be linked externally).
## 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
- Add 2.0.0 release notes and illustrations to `twenty-website-new`
- Remove old release mdx files from the legacy `twenty-website`
- Fix the resources-menu "Releases" preview to pull the latest release
dynamically, using the first (hero) image of the latest mdx
## Test plan
- [ ] Open any page on `twenty-website-new`, hover "Resources" → the
Releases preview shows "See what shipped in 2.0.0" with the Build-an-app
hero illustration
- [ ] Visit `/releases` and confirm 2.0.0 renders with all five sections
and images
- [ ] Confirm legacy `twenty-website` no longer ships the deleted
release pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Adds `IS_AI_ENABLED` to `PUBLIC_FEATURE_FLAGS` so it appears in
**Settings > Lab** as a self-serve toggle for users
- Includes a cover image (`is-ai-enabled.png`) hosted on
`twenty.com/images/lab/`, following the existing convention for lab
feature flag images
- AI is now ready for public beta — users can enable it directly from
the Lab
## Test plan
- [ ] Verify the AI card appears in Settings > Lab with the cover image
banner
- [ ] Toggle AI on/off and confirm the feature flag is persisted
- [ ] Confirm AI features (agent chat, etc.) activate when the flag is
enabled
Made with [Cursor](https://cursor.com)
## Summary
This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error
handling patterns across the codebase to use a new centralized
`useSnackBarOnQueryError` hook.
## Key Changes
- **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to
`^3.11.0` in root package.json
- **New Hook**: Added `useSnackBarOnQueryError` hook for centralized
Apollo query error handling with snack bar notifications
- **Error Handling Refactor**: Updated 100+ files to use the new error
handling pattern:
- Removed direct `ApolloError` imports where no longer needed
- Replaced manual error handling logic with `useSnackBarOnQueryError`
hook
- Simplified error handling in hooks and components across multiple
modules
- **GraphQL Codegen**: Updated codegen configuration files to work with
Apollo Client v3.11.0
- **Type Definitions**: Added TypeScript declaration file for
`apollo-upload-client` module
- **Test Updates**: Updated test files to reflect new error handling
patterns
## Notable Implementation Details
- The new `useSnackBarOnQueryError` hook provides a consistent way to
handle Apollo query errors with automatic snack bar notifications
- Changes span across multiple feature areas: auth, object records,
settings, workflows, billing, and more
- All changes maintain backward compatibility while improving code
maintainability and reducing duplication
- Jest configuration updated to work with the new Apollo Client version
https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
---------
Co-authored-by: Claude <noreply@anthropic.com>
## 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
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
## Summary
- Add E2B logo to the Thanks section (after Crowdin, same height)
- Rename section heading from "Does the world need another CRM?" to "Why
Twenty" for a more confident, direct tone
## Test plan
- [ ] Verify E2B logo renders correctly on GitHub README
- [ ] Confirm all other logos still display properly
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
This PR addresses TODO comments and improves code quality:
### 1. PullRequestItem.tsx
- Replaced `react-tooltip` with `twenty-ui` `AppTooltip` component
- Removed TODO comment
- Uses internal component library for consistency
### 2. MenuItemAvatar.tsx
- Refactored to use `MenuItem` internally, eliminating code duplication
- Removed about 63 lines of duplicate code
- Removed TODO comment as the merge is now complete
---------
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
## Summary
Moves the custom ESLint rules from `tools/eslint-rules` to
`packages/twenty-eslint-rules` for better organization within the
monorepo packages structure.
## Changes
- Move `eslint-rules` from `tools/` to `packages/twenty-eslint-rules`
- Use `loadWorkspaceRules` from `@nx/eslint-plugin` to load custom rules
- Update all ESLint configs to use the `twenty/` rule prefix instead of
`@nx/workspace-`
- Update `project.json`, `jest.config.mjs` with new paths
- Update `package.json` workspaces and `nx.json` cache inputs
- Update Dockerfile reference
## Technical Details
The custom ESLint rules are now loaded using Nx's `loadWorkspaceRules`
utility which:
- Handles TypeScript transpilation automatically
- Allows loading workspace rules from any directory
- Provides a cleaner approach than the previous `@nx/workspace-`
convention
## Testing
- Verified all 17 custom ESLint rules load correctly from the new
location
- Verified linting works on dependent packages (twenty-front,
twenty-server, etc.)
## Summary
This PR enforces the use of `@/` alias for imports instead of relative
parent imports (`../`).
## Changes
### ESLint Configuration
- Added `no-restricted-imports` pattern in `eslint.config.react.mjs` to
block `../*` imports with the message "Relative parent imports are not
allowed. Use @/ alias instead."
- Removed the non-working `import/no-relative-parent-imports` rule
(doesn't work properly in ESLint flat config)
### VS Code Settings
- Added `javascript.preferences.importModuleSpecifier: non-relative` to
`.vscode/settings.json` (TypeScript setting was already there)
### Code Fixes
- Fixed **941 relative parent imports** across **706 files** in
`packages/twenty-front`
- All `../` imports converted to use `@/` alias
## Why
- Consistent import style across the codebase
- Easier to move files without breaking imports
- Better IDE support for auto-imports
- Clearer understanding of where imports come from
## Release 1.12.0
This release includes:
- Revamped Side Panel - Now opens next to content instead of above it
- Granular Email Folder Sync - Choose which Gmail labels or Outlook
folders to sync
Changelog file:
`packages/twenty-website/src/content/releases/1.12.0.mdx`
Release date: 2025-12-02
## Release 1.10.0
This release includes:
- Calendar View for Objects - visualize records in a monthly calendar
view
- Dashboards in Labs (Beta) - create custom charts and visualizations
with workspace data
Changelog file:
`packages/twenty-website/src/content/releases/1.10.0.mdx`
Release date: 2025-11-11
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.
- Update user guide links for email integration, notes, tasks, and
API/webhooks sections to reflect new route structure
Currently, navigating using Algolia's search can result in 404s due to
broken links:
<img width="1105" height="601" alt="Screenshot 2025-10-22 at 10 36
55 AM"
src="https://github.com/user-attachments/assets/54ba762c-f616-4029-a100-0eca3f8cbd9e"
/>
Co-authored-by: StephanieJoly4 <stephanie@twenty.com>
This PR updates the troubleshooting documentation to address Javascript
heap out of memory error during first start of the twenty-server. Here's
a summary of the key changes:
- Added new workaround in troubleshooting.mdx addressing heap out of
memory error during twenty-server:start on WSL
- Created new sections
- Added new icons
- Created / Updated articles, focussed on use cases instead of only
features
- Added concrete examples of workflows to set up
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
## Release 1.8.0
This release introduces three major workflow enhancements:
### Workflow Iterator Node
- Ability to loop through items in workflows
- Process multiple records sequentially
- Perform actions on each item in a collection
### Workflow Bulk Select
- Select multiple records for manual trigger nodes
- Pass several records to workflow execution
- Works seamlessly with the new iterator node
### Workflow Search Node Limit
- Customize search result limit above 1
- Retrieve multiple records in a single search
- Enhanced compatibility with iterator node for processing results
---
Changelog file: `packages/twenty-website/src/content/releases/1.8.0.mdx`
Release date: October 16, 2025
- Removed a few visuals that are not necessary in the end (and not used
yet)
- Replaced visuals that were added yesterday with a new version,
matching the expected format