ae235d3b2432f00d2a77a54b6c87e3ac82747ce0
513 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
91b6cbd320 |
Update website release notes through 2.26 (#23736)
## Summary - Add weekly, user-facing release notes from 2.1 through 2.26. - Highlight completed, generally available product features and exclude Labs or rollout-gated work. - Update the Releases menu preview to the latest 2.26 entry. ## Before/After Before: production ends at 2.0. After: the changelog includes weekly releases through 2.26. <img width="2268" height="720" alt="before-after" src="https://github.com/user-attachments/assets/7ebf0154-596a-4846-b148-3d9aeff7cf0e" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23736?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. --> |
||
|
|
c25c0f4698 |
Add Enterprise plan and competitor pricing comparison pages (#23588)
Reworks the pricing surface in one PR: a new Enterprise tier on the
pricing page, five competitor pricing comparison pages, and the pricing
page section that links to them.
## Enterprise tier (pricing page)
- Third plan on both the cloud and self-hosting views: "from $50k /year"
with a "Talk to sales" CTA opening the existing contact modal.
- Cloud bullets: single-tenant isolation, IP allow-listing, SCIM
provisioning, dedicated support & SLA. Self-host: SCIM, air-gapped
deployment, LTS releases.
- Comparison table gains an Enterprise column (inherits Organization
values unless a row overrides) plus an Enterprise category of rows.
- New halftone building icon generated with the /halftone studio to
match the plan icon family.
- Pro and Organization pricing unchanged ($9/$19, in sync with Stripe).
## Competitor comparison pages
-
`/compare-pricing/{hubspot,salesforce,attio,pipedrive,microsoft-dynamics}`:
one shared template driven by a data file per vendor.
- Feature-by-feature cost table (competitor price + unlocking tier +
source link per claim, "checked on July 30, 2026" note, "spotted an
inaccuracy" link), a side-by-side bill for a 20-person team using the
PlanCard visual language with a savings badge cloned from the billing
toggle's -25% chip, and a one-line "fair play" note per vendor.
- Prices are 2026 list prices researched from vendors' public pricing
pages, billed annually. Routes registered in the website route registry
(indexed, sitemap, hreflang).
## Pricing page hub
- The salesfarce section intro becomes "Compare the real cost" with the
five comparison links using the footer's hover-marker link style. The
parody widget is unchanged.
Locale catalogs untouched for the i18n bot. Typecheck, lint, and the
website test suite pass.
Worth a second pair of eyes: the Pipedrive plan names/prices (2025
rebrand; their pricing page blocks fetchers) and the Salesforce "+30% of
spend" support/sandbox figures.
|
||
|
|
7fe59bf42d |
feat(website): send the referring partner with a client brief (#23351)
**Pairs with #23344** (`twenty-partners` v1.4.1), which adds the `referredByPartner` relation and the Discord notification. This PR is the sender; that one is the receiver. **Merge #23344 first.** Its schema is a non-strict `z.object`, so an unknown `partnerSlug` is stripped rather than rejected — shipping this one first degrades silently (attribution dropped) rather than breaking, but there is no reason to. Until #23344 is deployed, this field goes nowhere. No dependency in the other direction and no shared files: #23344 is entirely inside `packages/twenty-apps`, this is entirely inside `packages/twenty-website`. ## What this does A visitor can reach the client brief form from two places: the marketplace listing page, or a specific partner's profile. Until now both produced an identical payload, so the partner whose page drove the lead was lost. This sends the partner's slug along with the brief when the form was opened from a profile page. #23344 resolves it to a Partner record and links it to the created Opportunity. ## How it flows `PartnerProfileCtas` links to `/partners/brief?partner=<slug>` → `page.tsx` reads and normalizes the param → prop threaded through `ClientBriefPageContent` → `ClientBriefWizard` → `buildClientBriefRequestBody`. The slug is inert context, never a form field, so the wizard reducer and `ClientBriefState` are untouched. The three CTAs on `/partners/list` (`MarketplaceHeader`, `MarketplaceMatchCard`, `MarketplaceBriefPrompt`) stay bare — a brief from the listing page has no referring partner, and the notification labels it "Marketplace listing". ## Why `normalizePartnerSlug` exists `clientBriefRequestSchema` is a `z.strictObject`. Forwarding a malformed `?partner=` value straight into the body would fail validation for the **entire request** and lose the brief — a bad trade for an attribution field the visitor never saw. So the param is normalized at the boundary: array-valued params take the first entry, and anything not matching `[a-z0-9-]{1,100}` is dropped to `undefined` rather than passed on. The charset mirrors the app's `slugify` helper, which is what produced the slugs in the first place. ## Testing 8 new cases — 6 for the normalizer (well-formed, absent, empty, bad charset, over-long, repeated param) and 2 for the schema. Suite: 456 passing, up exactly 8 from a 448 baseline. `oxlint` and `oxfmt --check` clean; `next build` compiles with no type errors. Verified in a browser rather than asserted: opening a partner profile, clicking "Submit a brief", and completing the wizard produces ```json {"firstName":"Jane","lastName":"","email":"…","companyName":"NetZero Test Co","need":"…","partnerSlug":"netzero-systems"} ``` on `POST /api/client-brief` → 200. `LocalizedLink` preserves the query string across locale prefixing (`localize-href.test.ts:20` already covers this; confirmed live on the FR route). ## Deliberately not included - **CTA-level attribution.** Which of the three listing-page CTAs was used is not tracked. That is click analytics, a different concern from partner attribution. - **Length bounds on the other brief fields.** `country`, `seatCount`, `timeline`, `budgetRange` and `companyName` remain unbounded, as they were before this PR. Worth tightening, but pre-existing and out of scope here. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23351?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. --> |
||
|
|
f834b020b6 |
i18n - website translations (#23348)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23348?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
5948c167a0 |
i18n - website translations (#23196)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23196?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
cada1ef6d7 |
feat(website): live community stats, drop hard-coded fallback (#23047)
## Problem The menu's GitHub star and Discord member counts almost always render the hard-coded snapshot (49.6K / 6.6K, frozen June 2026), not live numbers. The render-time fetches run unauthenticated from Cloudflare Workers, whose egress IPs are shared across tenants; GitHub's unauthenticated quota is 60 req/hr per IP, so the call is effectively always rate-limited. Live prod today shows the frozen 49.6K GitHub count next to a live Discord count, confirming only GitHub is affected. ## Change - GitHub fetch sends `Authorization: Bearer $GITHUB_STATS_TOKEN` when the env var is set, moving it onto its own 5,000 req/hr quota. Discord keeps the public invite endpoint, which works fine from Cloudflare's IPs. - The hard-coded fallback is deleted rather than refreshed. `CommunityStats` fields are now `number | null`, resolved live -> last-good -> null, and the menu renders an icon-only chip when a count is genuinely unavailable. A fake number can never ship. - Last-good values persist in the worker's existing OpenNext R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`, key `community-stats/latest.json` outside the `incremental-cache/` prefix), so a third-party outage shows the numbers from the previous refresh. No new infrastructure. - Revalidation tightens from 1h to 15min (~4 GitHub calls/hour, shared cache entry across pages). - `MenuSocial`/`MenuDrawer` import `formatCompactCount` from its module directly: the community barrel now re-exports server-only code, and a client value-import would pull `getCloudflareContext` into the client bundle. ## Rollout - twentyhq/twenty-infra#795 passes the built-in Actions token at build time so prerendered pages ship with a real star count from the first request after deploy. - One manual step: set the `GITHUB_STATS_TOKEN` secret (fine-grained PAT, public read-only, no permissions) on the twenty-website-dev and twenty-website-prod workers. Until it exists, behavior degrades to today's minus the fake numbers. ## Tests 5 unit tests cover the resolution ladder: live wins, cache fills a failed fetch, null on cold-cache failure, both-fail serves cache without overwriting, nothing written when nothing succeeded. Verified against the dev server: menu renders live 53.3K / 6.9K. |
||
|
|
6b99bcea7f |
feat(partners): require Twenty experience on apply, drop Cal success (#23223)
## Summary - Add a dedicated **Experience** step to the partner apply wizard (milestones, ≥200-char narrative, proof URL) before commercials - Stop collecting `applicationNotes`; rename Expertise chrome away from “experience” - Replace the post-submit Cal.com embed with a review-and-reach-out thank-you so unqualified inbound no longer books intros automatically **Companion PR (app):** #23224 — Partner schema, submit persistence, triage views, Tally CSV mapper (`twenty-partners` v1.4.0). Land the app PR with or before this one. ## Test plan - [ ] Open apply modal: wizard order is identity → profile → expertise → experience → commercials - [ ] Experience step blocks continue without ≥1 milestone, narrative ≥200 chars, and a valid https URL - [ ] Submit creates/updates Partner with the three experience fields (with #23224 deployed) - [ ] Success screen has no Cal embed / book-later CTA - [ ] `npx jest --config=jest.config.mjs partner-application` passes locally (71 tests) --------- Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com> |
||
|
|
32041ce2e0 |
fix(website): restore condensed marketplace list card (#23161)
## Summary The condensed marketplace list-card design (sans-serif partner name, description excerpt, a single scope line, and a "View profile →" text CTA) was the intended glowup card, shipped by #22471/#22402. During the #23016 resync, the `PartnerCard.tsx` merge conflict was resolved in favor of main's rich card, so the condensed design never actually landed on `main`. This PR restores the condensed design, ported onto main's current data layer and helpers — none of #23016's profile/case-study/matching work is touched. ## What changed - `PartnerCard.tsx` — rewritten to render the condensed layout (sans-serif `PartnerName`, `CardIntro` description excerpt, `CardFoot` with a single `ScopeLine` and a mono `CardCta` "View profile →" text link, no chip rows, no money row, no LinkedIn icon). Reuses main's data layer and helpers directly: `richTextExcerpt`, `resolvePartnerScopeCards`, `PartnerAvatar`, `titleCaseFallback`, and the shared `CardFrame` shell (same entrance-animation/hover idiom already used by `MarketplaceMatchCard`). No new helper files were needed — everything the condensed design requires already exists on `main`. - `PartnerChipRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). - `PartnerMoneyRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). Kept untouched: `fetch-live-marketplace-partners.ts`, `marketplace-partner.ts`, `get-marketplace-partners.ts`, `marketplace-partners-source.ts`, all `PartnerProfile*`/case-study/matching files, `MarketplaceGrid.tsx` (already has the `repeat(N, minmax(0, 1fr))` grid columns on main — no change needed), and the shared label helpers `PARTNER_SCOPE_LABELS`/`SERVED_GEO_LABELS`/`SPOKEN_LANGUAGE_LABELS` (still used by `FilterBar.tsx`/`PartnerReachFacts.tsx`). Website-only change — no partners-app version bump, no `.po` catalog changes. ## Test plan - [x] `npx nx typecheck twenty-website` — clean - [x] `npx oxlint -c .oxlintrc.json .` (twenty-website) — 0 warnings, 0 errors - [x] `npx oxfmt --check .` (twenty-website) — clean - [x] `npx jest partners-marketplace` — 12 suites / 54 tests passing - [x] Verified locally against the running dev server (`/partners/list`): 16 partner cards render with the condensed layout (sans-serif name, excerpt, single scope line, "View profile →"), no chip rows/money rows/LinkedIn icons, real partner data renders correctly (e.g. 01GROWTH, Inc) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23161?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. --> |
||
|
|
cf5e6b7bad |
i18n - website translations (#23096)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23096?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
6742cfe861 |
Marketplace glowup — live partner profiles, case studies & matching (website) (#23016)
Rebuilds the partners marketplace on live CRM-backed partner data: real profiles, case studies, matching/scope cards, and a "match me" entry point in the grid. ## What changed - Marketplace grid and partner cards now fetch, rank, and filter live partner data instead of static fixtures - Partner profile pages render live profile data, including services, portfolio/case studies, and clients - Partner scope/matching cards on the profile page, plus a `MarketplaceMatchCard` as the first tile in the marketplace grid, routing into the client-brief flow - Rich CTA rail on partner profiles (calendar link, website, socials) built from live partner links - Markdown rendering (`react-markdown`) for partner descriptions and case study bodies, including proper heading rendering - Minor route/sitemap adjustments to support the live-data pages ## Architecture / notes This branch was 463 commits behind `main` and was resynced via a single merge (not rebase) to avoid re-resolving the same conflicts repeatedly. Several of the branch's earlier commits (client-brief wizard, `MarketplaceBriefPrompt`, `MarketplaceMatchCard`'s base styling, `PricingEngagementBand`) had already landed on `main` independently, in some cases refactored into shared components (`EngagementBand`, `MarketplaceCardFrame`, `createWebhookForwardingRoute`) — those conflicts were resolved by taking `main`'s already-shipped version. `PartnerCard.tsx` had diverged into two different designs (`main` gained chip rows / money row / LinkedIn icon; this branch gained the live case-study/portfolio data model with markdown descriptions and structured partner links); the resolution keeps this branch's data model (`description` as markdown, `links`/`linkUrls`) while adopting `main`'s card layout, adapting field references accordingly. `PartnerProfileCtas.tsx` keeps this branch's richer link-rail implementation since it's the one that matches the live data model already wired into `PartnerProfile.tsx`. This is the website counterpart to app PR #22929 (glowup, v1.3.0), already deployed to prod, and supersedes the closed drafts #22471 and #22402. Lint, format, targeted marketplace/client-brief jest tests, and `nx typecheck twenty-website` all pass after the merge. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23016?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. --> |
||
|
|
8d84a0b9f3 |
feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context Follow-up to #22609. Lets a non-admin developer claim ownership of a public Twenty app they published to npm, then request a marketplace listing that a server admin reviews. Marketplace state is per-instance for now. ## Claiming - Developer tab gets a **Claim an application** section: look up an unclaimed npm app by package name or universal identifier. - Ownership is proven with GitHub OAuth against the package's npm provenance (trusted publishing): the connected account must own the GitHub account or organization the package was published from. - Errors from the GitHub callback come back as a code and are shown inline with a link to the relevant documentation. - The old one-click claim stays admin-only. - A **Sync catalog** button triggers a catalog refresh instead of waiting for the hourly cron. - Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag. ## Listing requests - Catalog-synced apps are created **unlisted**; a data migration unlists previously auto-listed unclaimed npm apps (owned or vetted rows are left untouched). - Owners request a listing from the Distribution tab (logo + description required); a server admin approves or rejects it from a **Listing requests** section in the Admin Panel. ## Screenshots <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/> <img width="1512" height="828" alt="image" src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
80ff1716e5 |
i18n - website translations (#23005)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23005?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
ccbd3b6c46 |
Client brief wizard — /partners/brief (#22291)
## Brief — website (Release ① of the brief + glowup rollout) Public client-brief wizard at `/partners/brief`, plus the marketplace entry points (match-me card, brief prompt/link, brief CTAs across partner surfaces). **Backend already shipped:** the `submit-client-brief` logic function merged in #22290 (v1.2.0) and is live in prod, so this PR is **website-only** and needs no app deploy. Rebased onto current `main` (was ~341 commits behind); lint + format pass locally, typecheck/tests via CI. ### Release sequence (do not break) 1. **① Brief website — THIS PR.** Independent; backend already live in prod. → merge → website deploy. 2. **② Glowup app → prod** (#22470). Rebase onto `main` (SDK 2.21), apply deterministic-id handling, bump 1.2.10 → 1.3.0, `deploy` + `install` on `partner-twenty-com`, set new app variables, refresh partners-doc. **This is the gate for ③.** 3. **③ Glowup website** (#22471) — only **after ② is LIVE on prod** (it reads the new partner links / services / case-study objects). → merge → website deploy. 4. Reconcile #22637 (partners-traffic-web) with ③ — both touch `partners-marketplace/*`. Draft — do not merge until vetted. |
||
|
|
b06f3c7f8f |
fix(website): query isVetted so the apps marketplace renders again (#22859)
## What Production `/apps` renders no apps. The marketplace queries still request `isFeatured`, but #22674 renamed that flag to `isVetted` across the server schema (2-20 instance command renames the DB column — same flag, same data, trust-signal semantics). Production (2.21) rejects the query: ``` Cannot query field "isFeatured" on type "MarketplaceApp" ``` The transport throws on GraphQL errors, `fetchMarketplaceApps` catches and falls back to `[]`, so the page renders the empty state. `/apps/[slug]` detail pages degrade the same way. ## Fix Rename `isFeatured` -> `isVetted` across the website marketplace module: both queries, the API response types, the `MarketplaceApp` domain type, and the vetted-first sort. 4 files, no behavior change beyond restoring the data (same column, same values). The catch-all `[]` fallback is intentionally left in place. ## Verification - Corrected query run against `https://api.twenty.com/metadata`: returns the 3 live apps (People Data Labs, Last contact, Call Recorder), `isVetted: true`. - Rename provenance confirmed: #22674 is a pure rename (paired diff, symmetric column rename, `previousName: 'isFeatured'` marker on the entity). - `nx typecheck twenty-website` + `nx lint twenty-website` green. Takes effect on the next website deploy (`force-dynamic` route, 300s revalidate). |
||
|
|
9da5383289 |
i18n - website translations (#22683)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22683?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
16e5d2b9d0 |
feat(website): add public apps marketplace with one-click install (#22611)
<img width="1335" height="570" alt="image" src="https://github.com/user-attachments/assets/85bbe656-7525-4800-9ae6-bc59445ccf48" /> <img width="1512" height="723" alt="image" src="https://github.com/user-attachments/assets/6494c539-a3e0-4e95-9df5-ee7559c098ab" /> <img width="1512" height="813" alt="image" src="https://github.com/user-attachments/assets/4cb6adea-d35b-470a-9bfd-d47b2a514e8c" /> <img width="1512" height="739" alt="image" src="https://github.com/user-attachments/assets/757ea3e0-bff9-4482-9a71-3e75949fd7e7" /> <img width="1468" height="790" alt="image" src="https://github.com/user-attachments/assets/37365c56-47ae-4f08-a40f-32e5600a6c0f" /> ## What Adds a public `/apps` marketplace on twenty-website listing the vetted, Twenty-built apps. Each app card and detail page has a one-click Install button that deep-links to the in-app available-application page (`app.twenty.com/settings/applications/available/:universalIdentifier`), where authentication and permission consent are handled before installing. - New apps directory (`/apps`) and per-app detail pages (`/apps/[slug]`), with a category filter - Routing/sitemap entry for `/apps` and an Apps link in the Resources menu - The catalog is fetched from the Twenty GraphQL API (public `publicMarketplaceApps` / `publicMarketplaceAppDetail` queries), defaulting to `api.twenty.com`, and degrades to an empty state on failure. No app info is duplicated in the website; logos and screenshots come from the catalog's CDN URLs. ## Dependency This is the website half of the split. It consumes the public queries added in the server PR #22647, which should merge first. --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: martmull <martin@twenty.com> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com> Co-authored-by: Aressand <97886962+Aressand@users.noreply.github.com> Co-authored-by: Brahm Lower <bplower@gmail.com> Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Pratik Mahajan <Pratik@mahajan.xyz> Co-authored-by: neo773 <neo773@protonmail.com> Co-authored-by: Deepak kumar maharana <100968930+deep231w@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> |
||
|
|
8a4bcd1445 |
(Billing for self hosts) Tie enterprise key to server (#22464)
# Enterprise key: bind to a server, free dev instances, self-serve transfer, shorter license ## Summary Enterprise keys were being reused across multiple instances (e.g. one prod + one dev, or several environments), which broke seat accounting and made licensing ambiguous. This PR ties each enterprise key to a **single server**, while giving customers a legitimate, self-serve way to run a **free development instance** and to **move their key** when they replace a server. ## Product behavior ### 1. Enterprise key is bound to one server - The first server to validate an enterprise key **claims** it (claim-on-first-use). From then on, that key is bound to that one server (until unbound - see 3.). - Any other instance that presents the **same key from a different server is hard-rejected**: it does not receive a license, so enterprise features stay off there. - Each instance has a stable server identifier. If one isn't set, the instance generates and persists one automatically on first validation (in keyValuePair table), so existing customers generally don't need to do anything (unless they have disabled config variables in db then they should add it to .env). ### 2. Free development instance - Every enterprise subscription gets **one free, non-billable development instance** in addition to its production instance. - An instance registers as development by declaring its instance type as `development` (done by default when validating the enterprise key, then can be toggled from UI or by updating value in keyValuePair table). - The free dev slot is only granted while there is an **active production instance** on the same subscription (so it's a perk for paying customers, not a way to run for free). - Only **one** dev instance can be active at a time per subscription, and it is **not counted as a billable seat**. ### 3. Self-serve unbind / rebind (transfer) - Admins can **release** the binding from the enterprise settings, which frees the key so it can be **claimed by a new server**. - This is the intended path when **sunsetting an instance and standing up a new one** (migration, re-hosting, disaster recovery): release on the old/dead box, then the new box claims it on its next validation. - To prevent abuse, releases are **rate-limited (10 per rolling 30 days)**; hitting the limit shows a clear message. ### 4. Automatic release of dead servers - If a bound server stops checking in for **14 days**, its binding is considered stale and is **auto-released**, so a replacement can claim the key without any manual step. This covers the case where the old server is already gone and can't release itself. ### 5. Shorter license validity (30 → 7 days) - The license (validity token) now expires after **7 days** instead of 30. The daily background refresh keeps healthy instances licensed transparently. - This limits the value of copying a license from one instance to another, since a copied license now stops working within a week. ### 6. License issuance is rate-limited - Issuing a new license is capped at **twice per 24h, independently for production and for development**. This tolerates the normal daily refresh (including small drift between runs) while blocking bursts of license minting for cloned instances. - Hitting this limit never revokes an existing, still-valid license — the current one keeps working until it expires; the manual "refresh" button just reports that the daily limit was reached. ## What changes for existing self-hosted customers **If you run a single production instance with one enterprise key:** nothing to do. On the next validation your instance reports its server identifier, claims the binding, and keeps working. **If you reuse one key across several instances (e.g. prod + dev, or multiple environments):** only the **first** instance to validate keeps its license. The others will **lose enterprise features**. To migrate: - Keep your production instance as-is (it claims the binding). - For a secondary/testing box, mark it as a **development instance** (set the instance type to `development`) to use the free dev slot — no extra cost. - If you genuinely need multiple production instances, you'll need **separate subscriptions/keys** for each. **If you're replacing a server (decommissioning + rebuilding):** - **Release** the binding from enterprise settings on the old instance, then start the new one — it will claim the key automatically. - If the old server is already gone, just wait for the **14-day auto-release**, or contact support. **Legacy instances that can't persist a server identifier automatically:** set the server identifier explicitly in your environment configuration (the instance logs a message telling you to do so). **Offline instances:** because licenses now last 7 days, an instance that can't reach our licensing endpoint for more than a week will lose enterprise features until it can check in again. > A migration email will be sent to affected customers separately. ## Technical implementation (brief) - Binding state lives in the **subscription's billing metadata** (bound server id + last-seen timestamps for prod and dev, release timestamps, and license-issuance timestamps). No new database is introduced on the licensing side; the billing provider's subscription metadata is the source of truth. <img width="976" height="413" alt="metadata_3" src="https://github.com/user-attachments/assets/ccc64822-e177-4223-a65a-4a4602aedf0e" /> - On each validation, a pure **binding resolver** takes the reported server id + instance type + current metadata and returns `allowed` (with the metadata to persist and whether the seat is billable) or `rejected`. It handles claim-on-first-use, staleness/auto-release, the dev-requires-active-prod rule, and the single-dev-slot rule. - **Rate limits** (release + license issuance) use a shared sliding-window helper stored as pruned timestamp lists in the same metadata, so the metadata self-cleans and never grows unbounded. License issuance uses **separate windows per instance type**. - The self-hosted instance **generates and persists a server identifier** if none is configured, and sends it (plus instance type) as instance metadata on validation. - A rejected binding returns a specific error code; the instance **revokes its stored license** on that code. A license-issuance rate-limit instead **throws a typed exception that surfaces to the manual refresh** while leaving the existing license untouched; the daily refresh job swallows it. - License lifetime is a configurable duration (defaulted from 30 to **7 days**), clamped to the subscription's cancellation date when sooner. |
||
|
|
ed2b2f8911 |
feat: publish MCP & API discovery documents (well-known standards) (#22589)
## What & why
Makes Twenty's **MCP server** and **REST/GraphQL APIs**
auto-discoverable by catalogs (e.g. integrations.sh) and AI agents,
using vendor-neutral open standards rather than a proprietary manifest.
The tricky part is that Twenty is **multi-tenant and the REST OpenAPI is
generated per workspace** (it reflects each workspace's custom objects,
and with no token even the base schema is empty). So there is no single
public URL that describes the full API contract. This PR solves that
with two complementary layers.
## 1. Static standards on `twenty.com` (`twenty-website`)
The brand-level catalog entry, using `{your-workspace-url}` placeholders
since `twenty.com` is not a workspace host:
- `public/.well-known/mcp/server-card.json` — MCP Server Card (SEP-2127)
- `src/app/.well-known/api-catalog/route.ts` — RFC 9727 linkset (route
handler so the `application/linkset+json` content type survives the
global `nosniff` header)
- `public/llms.txt` — LLM-readable overview
## 2. Dynamic per-host serving from `twenty-server`
A new `well-known` core module serves the same documents built from the
**request host**, so every workspace subdomain, custom domain, and
self-hosted instance advertises its own **real, connectable** endpoints
(`https://{that-host}/mcp`, its live `/rest/open-api/core`, etc.) — no
placeholder:
- `GET /.well-known/mcp/server-card.json`
- `GET /.well-known/api-catalog`
Both are public + CORS + cached. The api-catalog's `service-desc` points
at each host's **live** per-workspace OpenAPI — the honest answer to
"it's generated per workspace" (real endpoint, real custom objects,
still token-gated). The `version` comes from `APP_VERSION`.
The two layers are complementary: the static one serves
catalog/marketing discovery at the brand domain; the dynamic one serves
connecting clients the real endpoints — which is where the MCP spec
expects the server card to live (same origin as `/mcp`).
## Refactor
Extracted the request→base-URL logic that `OAuthDiscoveryController` had
as a private method into a shared
`src/utils/get-request-base-url.util.ts`, now used by both it and the
new controller.
## Notes
- Docs URLs are sourced from the shared `DOCUMENTATION_BASE_URL`
(server) and the `SITE_URLS` registry (website) rather than hardcoded.
- MCP endpoint, transport (`streamable-http`), and protocol version
(`2025-06-18`) are read from the existing MCP constants.
- OAuth resource metadata (`/.well-known/oauth-protected-resource`)
already existed and is unchanged.
## Testing
- `twenty-server` unit tests for the builders and controller (host
derivation, version fallback, linkset shape) — passing.
- `nx typecheck twenty-server` — passing.
- `oxlint` + `oxfmt` clean on both packages; website `check-conventions`
OK.
https://claude.ai/code/session_01F6g7kefcfpjXSZjH6cwqhi
---
_Generated by [Claude
Code](https://claude.ai/code/session_01F6g7kefcfpjXSZjH6cwqhi)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22589?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. -->
|
||
|
|
049a98a36a |
fix(website): restore footer CTA buttons on the light card inside the dark stage (#22447)
## What The footer's **Talk to us** / **Get started** CTAs regressed: filled rendered white-on-white (only the label showed) and outlined vanished entirely. Resolves [this](https://github.com/twentyhq/core-team-issues/issues/2634) issue. ## Why #22241 marked the footer root as a dark menu-surface (`data-scheme="dark"`) so the sticky menu adapts over the dark footer stage. But the footer's content sits on a **white Card inside that root**, and the button's dark override is a *descendant* selector (`[data-scheme='dark'] &`) — so it leaked into the card. Filled → white fill + black label (invisible fill on white); outlined → white stroke + white label (fully invisible). The card's text was fine because it uses the light default semantic vars; only the buttons key off the raw attribute. ## Fix - Mark the white `Card` as `data-scheme="light"` — it *is* a light surface. The root keeps `data-menu-surface`/`data-scheme="dark"`, so **menu adaptation is unchanged**. - Add a button override scoped to `[data-scheme='dark'] [data-scheme='light'] &` — a light surface *nested inside* a dark one. It's higher specificity than the dark rule and matches **only** this footer case, so a dark card nested in a *light* section (e.g. `HelpedCard`) is never affected. No other button changes. Result: filled = black fill + white label, outlined = black stroke + black label — matching the design. ## Testing - `nx typecheck twenty-website` ✓ · `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - Reviewable on the PR preview (footer CTAs, plus menu/FAQ/hero/signoff buttons unaffected). |
||
|
|
63d092a31b |
fix(website): center the User Guide nav preview image (#22445)
## What The **User Guide** item in the Resources dropdown rendered its preview image off-center (anchored top-left with a gap below the halftone). ## Fix Set `imagePosition: 'center'` on the User Guide preview so the halftone book is centered and fills to the bottom of the frame, matching the others. ## Testing - `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - `nx typecheck twenty-website` ✓ |
||
|
|
8b6bd34a17 |
i18n - website translations (#22436)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22436?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
3474d75b56 |
feat(website): add Product to menu + footer nav, move Why into Resources dropdown (#22429)
## What Restores the **Product** link to the site nav (removed in #21794), reversing that commit's nav-structure change: - **Menu** — Product is the first top-level item (in place of Why); **Why** moves back into the **Resources** dropdown with its `why.webp` preview (`IconBulb`, "Why teams choose Twenty"). - **Footer** — Product added to the **Sitemap** group (after Home). The `/product` and `/why-twenty` routes already exist and are in the sitemap; only the nav data changed. The rest of #21794 (dropdown frame height, preview assets, current-page highlight) is untouched. ## Notes - New `msg` strings (`Product`, and Why's restored strings) are left to CI / the i18n bot to extract + translate — no catalog changes here. ## Testing - `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - `nx typecheck twenty-website` ✓ |
||
|
|
1b06532cb1 |
fix(website): tighten product-feature spotlight height and align bento spacing (#22428)
## What Design polish on the product page's `ProductFeature` bento, from designer review: - **Spotlight height** — the first (spotlight) card's visual was `min-height: 420px` on desktop vs the grid cards' `340px` (80px taller), so it towered over the rest. Now **340px**, matching the grid cards. - **Spacing consistency** — the spotlight visual used a uniform `margin` (bottom margin included), unlike the other cards' `CardVisualFrame` (`… 0` bottom). Removed it so the visual→content gap is consistent across every card. - **Gap** — bumped the visual→content gap to `spacing(6)` (**24px**) on desktop for all cards. ## Testing - `nx lint twenty-website` ✓ (oxlint + oxfmt + check-conventions) - `nx typecheck twenty-website` ✓ |
||
|
|
e69c3ae5ce |
fix(website): bump @opennextjs/cloudflare to 1.20.0 (R2 deploy on Node 24) (#22266)
## What The twenty-infra **"Deploy Website"** workflow has failed every run since 06-26 — at OpenNext's R2 incremental-cache step, **not** the build: ``` Failed to provision remote R2 bucket "twenty-website-cache-dev" for binding "NEXT_INC_CACHE_R2_BUCKET": Failed to check whether bucket exists: … Premature close ``` ## Root cause `@opennextjs/cloudflare`'s `ensureR2Bucket()` calls the Cloudflare SDK's `r2.buckets.get()`. On **Node 24** (which the deploy pins) undici truncates the **gzip-compressed** Cloudflare API response → `Premature close`. The SDK's own retries don't help (it's systematic, not flaky), and pre-creating the bucket doesn't help (it always `.get()`s first). This is **pre-existing and unrelated to the multi-locale change** (#22257): the *Build Worker* step succeeds, and the identical error appears on 06-26 runs (two days before that merged). ## Fix opennext **1.20.0** fixes this precisely — it passes `defaultHeaders: { "Accept-Encoding": "identity" }` to the Cloudflare SDK client, so the API returns **uncompressed** responses (no decompression → no premature close). **Node 24 is kept**, and no twenty-infra change is needed — the deploy runs `twenty`'s own `npx opennextjs-cloudflare`, so bumping the dep here is enough. `^1.0.0 → ^1.20.0`. **1.20.0, not the latest 1.20.1**, because the repo's `npmMinimalAgeGate: 3d` still quarantines 1.20.1 (published 06-26); 1.20.0 (06-25) is past the gate and carries the same fix. ## Verification - Resolved to `1.20.0`; confirmed `Accept-Encoding: identity` is in the installed `dist/cli/utils/ensure-r2-bucket.js`. - `nx typecheck twenty-website` green (OpenNext config API unchanged across the bump). - Diff is just `package.json` + `yarn.lock`. ⚠️ Full confirmation needs a **Deploy Website** run (exercises the build + the R2 provision step), which I can't trigger — please re-run it after merge. |
||
|
|
0569c490fd |
i18n - website translations (#22264)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22264?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. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
f8a9a2b249 |
i18n - website translations (#22263)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22263?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. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
012af11d77 |
feat(website): ship all documentation locales (multi-locale site) (#22257)
## What
The marketing site now serves every language the **documentation** ships
— 14 locales (`en, fr, ar, cs, de, es, it, ja, ko, pt, ro, ru, tr, zh`),
up from 3 (`en, es, fr`).
## How
- **Single source of truth.** `WEBSITE_LOCALE_LIST` derives directly
from `DOCUMENTATION_SUPPORTED_LANGUAGES` (`twenty-shared/constants`).
Add a documentation language → it flows to the website automatically.
- **Off `APP_LOCALES` entirely.** The website locale type is now
`DocumentationSupportedLanguage` (short codes), so a locale **is** its
URL segment — no short↔full mapping, and no `pt-BR`/`zh-CN` ambiguity to
resolve.
- **Removed the indirection this exposed** (it only existed because
`AppLocale` was a superset of the deployed set):
- `locale-to-url-segment` / `locale-by-url-segment` (locale == segment)
- `get-locale-messages` pass-through → callers read `MESSAGES_BY_LOCALE`
directly
- the `messages-by-locale` runtime guard → a total
`Record<DocumentationSupportedLanguage, Messages>` (a missing catalog is
now a **compile** error, not a runtime throw)
- `isWebsiteLocale` → a `string → DocumentationSupportedLanguage` type
guard
- the vestigial language-code `split('-')` in `locale-display-name`
## Catalogs
- Renamed `es-ES → es`, `fr-FR → fr`; added 11 new locales (untranslated
for now → **English fallback**).
- `crowdin-website.yml` switched to `%two_letters_code%`.
- Regenerating catalogs also synced `en.po` with current source
(`Boolean` / `Date & Time` / removed `Fields widget` from the
already-merged #22249).
- `ci-website` is unchanged — no `lingui:compile` step added; catalogs
stay committed.
## Testing
- `typecheck` · `lint` (check-conventions + oxlint + oxfmt) · 347/347
tests — all green. PR CI runs exactly lint + typecheck + test.
## Follow-up (out of repo)
Enable the 11 languages on **Crowdin project 4** so `website-i18n-pull`
backfills real translations. Until then, the new locales render with
English fallback (correct behavior).
|
||
|
|
77c84815ef |
feat(website): rework the product-stepper Layout visual (#22249)
## What Reworks the **"Layout" step** visual of the product-page stepper (the floating record-layout-editor scene) to match the Figma references and read as a premium, layered composition. Data is unchanged — this is purely the look. ## Figma alignment - **Fields editor** — the inline-edit field row now keeps the field icon + a bordered input + blue **Done** + the eye / ⋮ (instead of a floating box); **"New fields"** renders as a uniform field row; section headers get an overflow menu; field types read `Boolean` / `Date & Time`. Dropped the redundant "Fields widget" header label. - **"General" widget** — header chevron, the real `anonymousFelix` avatar on Account Owner (reusing the shared asset the other product-feature visuals use), a **Revenue** row, and a money-bag icon for the currency field. ## Premium composition - The nav, record, and Fields editor read as one elevated **z-stack** (back → front): the nav is pushed right so its edge tucks under the record (lowest z-index), the record sits above it, and the editor stays in front — each casting a progressively stronger shadow so the depth ordering is unmistakable. - The **"New record / Enrich / Edit actions"** bar is centered over the nav–record seam (`translateX(-50%)`) to tie the layered stack together at the top. ## Testing - typecheck · oxfmt · oxlint · check-conventions — green. - Verified visually with headless Playwright on `/product` (Layout step). <img width="697" height="734" alt="image" src="https://github.com/user-attachments/assets/58ae7ffb-13d1-4310-bbdc-864589f8bc43" /> |
||
|
|
4840233a1c |
i18n - website translations (#22244)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22244?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. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
03f3789d13 |
feat(website): menu adapts to the section beneath it (#22241)
## What The sticky menu now adopts the color scheme of whatever section sits beneath it as you scroll, across every page. Background, logo, and buttons ease between schemes for a smooth handoff. ## How - **Declarative contract** — sections opt in with a `data-menu-surface` attribute (`SectionShell`, the footer, and the product hero's mobile sections); `useActiveSurfaceScheme` tracks which surface sits under the menu's bottom edge (`MENU_HEIGHT_PX`) and reports its `data-scheme`. - **One provider** — `MenuStyleProvider` lives in the `(site)` layout so every page adapts; the Menu resolves its scheme as `override ?? activeScheme ?? prop`. SSR seeds the prop to each page's first section, so there's no mount flash. - **Hero handoff** — the product hero keeps the menu via a per-frame override only while its track still covers the nav band (`controlsMenu`); it releases to the observer *before* the track clears the bar, so the menu stays opaque on exit and never flashes the halftone backdrop rising behind it. The menu's `backdrop-filter` was removed (a no-op over the opaque menu, and a GPU compositing artifact). ## Also in this PR - **Menu folder reorg** to the `product-feature` convention: `components/`, `effect-components/`, `data/`, and `types/` (one domain type per file). - **`MENU_HEIGHT_PX` token** replacing the literal `64` that was duplicated across four files (menu row, the hero's scroll model and component, the observer). - **`findActiveSurfaceScheme`** extracted as a pure, unit-tested function (inclusive top / exclusive bottom, first-match, no-match, null-scheme). - **Mobile AI-section fix** — the mobile hero sections now declare the surface contract, so the menu adapts dark over the AI block on mobile (it previously stayed light). Their color is driven from `data-scheme` (single source) rather than a parallel prop. ## Testing - Jest — 16 tests (scroll model + surface-selection util). - Headless Playwright (mobile 390px) — menu `light` over the intro → `dark` over the AI section; section colors unchanged (`#fff` / `rgb(20,20,20)`). - Desktop unaffected — the mobile sections are `display:none` (zero rect), so the observer skips them and the hero override path is untouched. - Gates green — typecheck, oxlint, oxfmt, check-conventions. |
||
|
|
bf7a766935 |
i18n - website translations (#22234)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22234?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
ad1ac4f3c5 |
i18n - website translations (#22225)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22225?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
d58ec64e10 |
feat(website): swap product Demo for a register Signoff; make signoff headings width-driven (#22232)
## Summary
Reworks the product page's closing section, and in doing so makes every
signoff closer width-driven.
### 1. Replace the Demo section with a register Signoff (`6a5f0c34bf`)
The product page ended on a static `AppPreview` mockup ("Try it live").
Swap it for the shared `Signoff` closer — a focused register CTA, the
same component the customers / why-twenty / partners pages use — so the
page ends by driving sign-ups instead of re-showing the product.
- New `ProductSignoff`: heading "Start moving faster today.", a
supporting line, and the **Get started + Talk to us** pair (matching
`CustomersCatalogSignoff`). Uses `SITE_URLS.appWelcome` per the
site-urls rule rather than inlining the URL.
- Removes the now-unused `product-demo` section and its 192KB background
asset.
### 2. Make Signoff headings width-driven (`0365c0d2ea`)
The signoff headings forced their two-line break with a literal `\n` in
the translated string — against the site's typography principle (fluid
type + `text-wrap: balance`, no `<br>`).
- `Signoff` now carries one default **`615px`** heading measure; all
five closers drop `\n`.
- The width was **measured, not guessed**: each heading was rendered
headlessly in the real production fonts (Aleo 300 body, Host Grotesk 300
accent — each heading mixes both) at the desktop size with `text-wrap:
balance`, then I found the common window where every heading breaks
identically: whyTwenty `[565–820]`, customers `[425–800]`, partner
`[380–730]`, product `[380–660]` — `615` sits in all of them. Each was
then verified to reproduce its exact current break at 615px.
- **Affects 4 other pages** (why-twenty, customers, customers/[slug],
partners) — all verified to render identically, and headings now reflow
on narrow viewports instead of being pinned by `\n`.
- Drops the one-off `headingMaxWidth` prop (now unused).
## Test plan
- oxfmt, oxlint, check-conventions, typecheck — all green.
- Headless measurement confirms all five signoff headings reproduce
their current desktop break at 615px (no visual regression).
- Visually confirmed the product closer and the four sibling signoffs.
|
||
|
|
fea2b8736f |
feat(website): rebuild dashboard visual faithful to twenty-front (#22218)
Rebuilds the product-feature **DashboardVisual** to mirror
twenty-front's dashboard widgets, with colors traced to twenty-front's
actual source rather than eyeballed.
## Widgets
- **Bar — "Deals by month"**: single-series `blue8` (twenty-front's
`GRAPH_DEFAULT_COLOR`), dashed `4 4` gridlines, value labels, nice
rounded Y-ticks. Replaces the old stacked bar (stacked bars aren't used
in twenty-front).
- **Donut — "Deals by stage"**: the real opportunity pipeline
(New/Screening/Meeting/Proposal/Customer), each segment colored by that
stage option's own color from the metadata
(`red/purple/sky/turquoise/yellow`), with a center total and a paginated
horizontal legend.
- **KPIs**: big-number cards (Revenue YTD / Avg deal size / Win rate).
## Responsive — `mediaUp('md')`
The dashboard is the full-width spotlight tile, whose frame is short
below md and grows to 420px at md+. So the layout keys off md: below it
collapses to **2-up KPIs + a full-width bar** (donut and the 3rd KPI
hidden, smaller breadcrumb), and the spotlight frame's mobile min-height
is bumped so the bar has room; at md+ the full 3-KPI + side-by-side
layout returns. The donut caps at its size and shrinks with its
container.
## Notes
- Follows the `product-feature` conventions (per-visual folder,
one-export-per-file, split types).
- `Tiles.tsx`: one-line spotlight mobile min-height bump (only the
dashboard uses the spotlight tile).
- typecheck + lint + build all green.
<img width="1148" height="638" alt="image"
src="https://github.com/user-attachments/assets/1eb8f120-88c2-45d1-adf9-ec00abe11006"
/>
|
||
|
|
7caf2a14c5 |
feat(website): show partner location instead of served regions in marketplace (#22224)
## Context On the partners marketplace, the eyebrow shown below each partner's name (on both the list card and the profile header) displayed the served regions (e.g. `APAC`, `EUROPE`). The partner's actual location was buried in the "Where & how" facts list. The location is more useful at a glance, so this swaps the two. ## Changes **List card (`PartnerCard`)** - The eyebrow below the name now shows the partner's city and country instead of the first served region. **Profile page** - `PartnerProfileHeader`: the eyebrow near the title now shows the real location (city, country). - `PartnerFactsList`: the served regions move into the "Where & how" section as a `Regions` row, taking the place of the now-redundant "Based in" row. The `Regions` chip rows on the card are unchanged. https://claude.ai/code/session_01FQRRfvPpmNsjAAnZeDc5sy --- _Generated by [Claude Code](https://claude.ai/code/session_01FQRRfvPpmNsjAAnZeDc5sy)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22224?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. --> |
||
|
|
1b4bddba6f |
fix(website): polish product hero AI transition (panel reveal timing + title wrapping) (#22223)
## Summary Two desktop/tablet polish refinements to the product page hero's hero→AI transition (`product-hero`). ### 1. Delay the Ask-AI panel reveal to after the wipe (`b638d600`) The Ask-AI side panel used to reveal *during* the dark "wipe up" (keyed to the morph `0.45 → 0.70`), so it competed with the rising black edge and piled onto the heaviest motion phase — rough on weaker laptops. It's now keyed to raw scroll progress over `[0.60, 0.70]` — the previously-idle hold after the wipe settles at `0.55` — so it slides in on its own beat once the eye has followed the black up. `morphProgress` is pinned at `1` across the whole post-wipe stretch, so leaving the morph clock was the only way to time the panel *after* the wipe. The conversation playback moves with it, so the chat doesn't stream while the panel is still `width: 0`. ### 2. Keep the AI heading to three lines below desktop (`8ae69a4b`) The AI heading is the page's longest line. Below `md`, only the mobile copy renders, where the measure was frozen at `360px` while the heading font scales fluidly toward `md` — so on tablet widths the title crammed onto four lines, recovering only at `921px` when the `672px` measure kicks in. Adds an `sm`-breakpoint measure step (`560px`) scoped to the AI heading; the intro heading and the desktop measuring path keep their existing `360/672` steps. ## Test plan - `product-hero-scroll-model` jest suite updated and green (11/11), including a new post-morph panel-timing test. - Lint (`check-conventions` + `oxlint` + `oxfmt`) and typecheck green. - Visually confirmed: the panel reveals after the wipe settles, and the AI title holds three lines across tablet widths. |
||
|
|
e60d790990 |
i18n - website translations (#22203)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22203?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
85f64abb28 |
Frameless ProductStepper with twenty-front-faithful scenes (#22197)
## What Reworks the website's **ProductStepper** (the scroll-driven *Data model / Automation / Layout* section) to be frameless and to render all three scenes faithfully to twenty-front — real icons, labels, colors, structure, and connectors. ## Changes **Frameless + scaling** - Removed the shared white card/header; the three scenes now sit directly on the dark dot-grid stage. - Unified every scene on one `StageFit` primitive (fixed design box → scaled to fit), pixel-identical at full width and uniform on smaller screens. **Data model scene** - Real object schema (3 Standard + 2 Custom objects, real relation fields); replaced an invented "Investors" object with the real **Employment History** custom object (`IconBriefcase`, Company + Person relations) from the server seed. - Clean spanning-tree connections; removed the Standard/Custom badge; fixed card sizing + edge centering. **Workflow scene** - Real action labels + a logical flow: *Record is Created → Filter → Search Records → AI Agent → (Update Record · Send Email · Create Record)*. Dropped the iterator (a loop construct shown without a loop body). - Per-action icon colors matching twenty-front (trigger blue, flow green, record gray, send-email red, AI agent pink) on a gray tile. - Rebuilt the node to twenty-front's real anatomy and the connectors (source circle → `getBezierPath` → arrow marker) verbatim. **Layout scene** - Real workspace sidebar: real Tabler object icons, exact labels, the true default sidebar (6 objects + Workflows folder), `getIconTileColorShades` tile colors; dropped invented entries. - Record overview + Fields editor with correct field-type labels (Links, True/False, Date and Time); legibility + spacing tuning. **Icons** — replaced every hand-drawn approximation with real `@tabler/icons-react` / twenty-front object icons. **Misc** — smoother step-to-step transitions (translate + easing tokens). ## Testing Marketing visual; `lint` / `typecheck` / `build` green, and each scene reviewed visually against twenty-front. https://github.com/user-attachments/assets/997e1b95-55c0-401a-93a4-c70545577057 |
||
|
|
1284099940 |
i18n - website translations (#22162)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22162?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a87bc4bed6 |
i18n - website translations (#22160)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22160?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
5a657129f0 |
Split website stepper into home-stepper and product-stepper sections (#22149)
Reorganizes the flat `sections/stepper/` (30 files — two distinct steppers plus shared code) into two product-feature-style sections, and moves the shared code to the shared layers. - **`sections/home-stepper/`** — the home-page stepper. Renamed `Stepper` → `HomeStepper` (and the home components → `HomeStepperLottie` / `HomeStepperSteps` / `HomeStepperVisualFrame`) for symmetry with `ProductStepper`. Shell at the root + `components/`/`data/`/`utils/` + barrel. - **`sections/product-stepper/`** — the product-page stepper, same structure. The 3 files both steppers shared can't live in a shared *section* — `check-conventions` forbids a section importing another section. So they moved to the shared layers: - `StepperProgressRail`, `StepperSwipeDeck` → `ui/` - `useBreakpointStepSync` → `platform/motion` Both consumer pages repointed (`@/sections/home-stepper`, `@/sections/product-stepper`); the row-gap allowlist in `check-conventions.mjs` updated to the new paths; explanatory comments stripped across the moved files (CSS-in-template comments and `'use client'` kept). Pure reorganization — no behavior change. typecheck + lint + build all green. |
||
|
|
904d3d6bff |
i18n - website translations (#22158)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22158?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. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
551a792291 |
Rework website TasksVisual to match twenty-front tasks tab (#22143)
- Rework `TasksVisual` to match twenty-front's tasks tab: per-group bordered cards with row dividers, `TODO`/`DONE` headers, the `+ Add task` button, and twenty-front's checkbox, row, and target-chip styling. - Enable the checkbox to toggle a task between `TODO` and `DONE`. Marketing data kept intuitive (single assignee, full dates, action-led titles). <img width="859" height="608" alt="image" src="https://github.com/user-attachments/assets/085c225f-b121-44e6-aaec-553c02fd2367" /> |
||
|
|
d6556221d9 |
Tidy website ImportVisual markup and CSS (#22148)
Markup/CSS tidy-up of `ImportVisual` — no visual change. - Collapse the redundant `Grid` wrapper into `Root` (now `flex-direction: column`) and drop the no-op `justify-content: center` (the child was already full-width). - Hoist the duplicated `font-size: previewFontSize(md)` to the mapping `Row` (the `sm` example keeps its override). <img width="574" height="604" alt="image" src="https://github.com/user-attachments/assets/dcc72567-5c13-4d54-884a-f3120cc5a345" /> |
||
|
|
491b9e954a |
Rework website EmailsVisual to match twenty-front emails tab (#22146)
- Rework `EmailsVisual` to match twenty-front's emails tab: bordered thread card with dividers, `Inbox` header sizing, and twenty-front's row/sender/date colors and hard-clipped sender names (kept `+ Compose`). - Replace the "not shared" row with four fully-populated shared threads (subjects/bodies sourced from twenty-server's dev seed), two participants each, full `MMM D, YYYY` dates. <img width="575" height="607" alt="image" src="https://github.com/user-attachments/assets/68d1fd6d-e030-4f3c-b775-687f7e1ab173" /> |
||
|
|
20f8321dca |
Rework website FilesVisual to match twenty-front files tab (#22147)
- Rework `FilesVisual` to match twenty-front's files tab: bordered file card with dividers, `All` header sizing, and twenty-front's row background and date color (kept the bordered `+ Add file`). - Four files, one per category (pdf/xlsx/pptx/png), with full `MMM D, YYYY` dates. File-icon colors already matched `useFileIconColors`. |
||
|
|
feac2df216 |
chore(website): remove partners marketplace route lint guard (#22121)
## Summary Follow-up to #22120. Removes the `check-partners-marketplace-routes.mjs` lint guard and its `project.json` wiring — the profile fix is just `force-dynamic` on the page; the extra script is not needed. ## Changes - Delete `packages/twenty-website/scripts/check-partners-marketplace-routes.mjs` - Restore `project.json` lint command to run only `check-conventions.mjs` (as before #22120) ## Context The guard was added in #22120 but the removal commit did not land before merge. This PR cleans that up. No runtime behavior change. ## Test plan - [ ] `nx lint twenty-website` (or CI) passes without the removed script <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22121?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. --> |
||
|
|
500f441807 |
fix(website): force-dynamic partner profiles to stop OpenNext 404 cache (#22120)
## Summary Partner profile pages (`/partners/profile/[slug]`) returned **404 on every slug** on OpenNext/Cloudflare while `/partners/list` showed live partners from the same API. PR #21963 fixed the list with `export const dynamic = 'force-dynamic'` but only added `dynamicParams = true` on profiles. That is not sufficient on OpenNext — the Worker kept serving **cached prerendered 404s** even when `TWENTY_PARTNERS_API_KEY` was present at runtime. This PR mirrors the list page: **`force-dynamic` on the profile route**, plus a small lint guard so both marketplace routes stay dynamic. ## Root cause Partner data is fetched server-side from `https://partners.twenty.com/s/partners` using `TWENTY_PARTNERS_API_KEY`. That key is a **Wrangler runtime secret** (not in `dev.env` / `prod.env`, not available during CI build — by repo convention). | Route | Before | Behavior | |-------|--------|----------| | `/partners/list` | `force-dynamic` (#21963) | Fetches at request time on Worker → works | | `/partners/profile/[slug]` | static + `dynamicParams = true` | Build prewarm often empty; OpenNext served cached 404 | ## Fix - Add `export const dynamic = 'force-dynamic'` to `profile/[slug]/page.tsx` (keep `dynamicParams = true`). - Add `scripts/check-partners-marketplace-routes.mjs` — fails lint if list or profile drop `force-dynamic`. - Wire guard into `project.json` `lint` target (runs before existing `check-conventions.mjs`). **No infra changes.** We intentionally did not add a GitHub Actions secret for the API key — that would contradict the documented pattern (`wrangler secret put` only). ## Verification - [x] `node scripts/check-partners-marketplace-routes.mjs` → OK - [x] `npx jest src/partners-marketplace` → 36/36 pass - [x] Deployed to **dev** (`deploy-website`, env `dev`, ref `rk-partner-profile-404`) - [x] `curl -sI https://twenty-main.com/partners/profile/atlasprods-technologies-llp` → **HTTP 200** - [x] Browser: list → profile link loads ## Test plan - [ ] CI lint + tests green - [ ] After merge: deploy prod when ready (`environment: prod`, confirm `website`) - [ ] Spot-check `https://twenty.com/partners/profile/<slug>` → 200 ## Out of scope - Build-time `generateStaticParams` prewarm (would need a separate infra discussion; not required once profiles are `force-dynamic`) - Per-slug `/s/partner-by-slug` endpoint (optional perf follow-up) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22120?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. --> |
||
|
|
85406a58fb |
Use minimal babel presets for wyw to fix the website Cloudflare build (#21994)
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. |
||
|
|
16c9782c96 |
i18n - website translations (#22001)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22001?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. --> Co-authored-by: github-actions <github-actions@twenty.com> |