28adcbffb9cacea4e4ab4674b87cff8bdcd945b7
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. -->
|
||
|
|
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).
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
569d887d1e |
[Website] Cut over to the rebuilt site (#21825)
Renaming the package so any further PRs directed to the website are targeted to the reworked code instead of diverging. Once merged, I will start preparing this for deployment to dev to test before releasing to prod. Any improvements will also be applied to this package. I avoided making significant changes to API routes so nothing breaks, but will test it thoroughly today to confirm. That said, everything is ported - double checked. Big diff PR, impossible to review, but last one! No more rebuilds. |