Commit Graph

9 Commits

Author SHA1 Message Date
Rashad Karanouh 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.
2026-07-17 17:13:16 +00:00
martmull 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>
2026-07-08 19:12:59 +00:00
Marie 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.
2026-07-06 18:07:03 +02:00
Félix Malfait 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. -->
2026-07-06 17:53:05 +02:00
Abdullah. 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).
2026-06-28 14:39:59 +05:00
Abdullah. 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.
2026-06-27 12:58:50 +05:00
Abdullah. 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
2026-06-26 10:17:44 +02:00
Abdullah. 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.
2026-06-25 12:24:22 +02:00
Abdullah. 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.
2026-06-19 10:22:46 +02:00