Commit Graph

19 Commits

Author SHA1 Message Date
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
Marie 9de1b6330c feat(website - self hosts billing): add internal endpoint to reissue enterprise keys (#21660)
## Summary

Adds an internal support endpoint to regenerate a customer's enterprise
key
when they've lost the one issued at activation. The key payload is
deterministic
from the Stripe subscription, so this re-emits an equivalent valid key
without
any new state.

`GET /api/enterprise/reissue/<subscriptionId>/<secret>`

- Guarded by a shared secret (`ENTERPRISE_ADMIN_API_SECRET`), compared
in
  constant time and fail-closed when unset.
- Looks up the subscription in Stripe (for the licensee) and signs the
key with
  `signEnterpriseKey()`, reading `ENTERPRISE_JWT_PRIVATE_KEY` from the
  environment — the private key is never accepted from the request.
- No subscription-status gate: the key alone grants nothing. Feature
access
still requires a validity token, which `/api/enterprise/validate` only
issues
  after re-checking the subscription is active.

## Notes / follow-ups

- The admin secret travels in the URL path, so it can land in
server/proxy/CDN
access logs — rotate `ENTERPRISE_ADMIN_API_SECRET` if logs are ever
exposed.
- No audit logging yet; worth adding (who reissued which subscription,
when).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21660?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: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 20:42:07 +02:00
Rashad Karanouh 7afc991bd6 Partner application form: required fields, skills rework, fail-fast validation (#21710)
## Summary (twenty-website — partner application form)

- **Required fields**: website URL, city, hourly rate, minimum project
are now required (client step-gate + server zod) with `*` markers. The
final step validates before POSTing, so empty required fields fail fast
client-side instead of round-tripping.
- **Technical skills reworked to *complement* "What you cover"** (the
service categories) rather than duplicate them — now a small shown set +
a larger searchable-only pool of tools / technologies / industries.
Field hint clarifies the intent.
- **No competitor CRMs** in suggestions (Salesforce/HubSpot/Attio
removed); a guard test fails if one ever reappears. Migrations surface
as a generic "CRM migration".
- `Form.TagInput` gains an optional `searchPool` prop (autocomplete-only
entries, not rendered as chips).

Companion to the app-side PR #21709.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21710?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-18 14:29:35 +00:00
Rashad Karanouh 5a55021e26 feat(website): add /partners/apply standalone page (#21219)
## Summary

- Adds a standalone `/partners/apply` page — a shareable URL that opens
the partner application wizard full-page (no modal, no nav, no footer),
on a plain black background
- Adds a `slots` prop to `PartnerApplicationWizard` so it can render
outside a `Dialog.Root` context (Base UI), keeping the existing modal on
`/partners` completely untouched
- Surfaces logic function errors to the user: the API route now checks
the webhook response body for `ok: true`, so a silent backend failure no
longer shows a false success state

## Test plan

- [ ] `yarn jest --no-coverage` — 365/365 passing
- [ ] `npx oxlint -c .oxlintrc.json .` — 0 errors (1 pre-existing
warning unrelated to this PR)
- [ ] `npx oxfmt --check .` — clean
- [ ] `npx tsc --noEmit` — clean
- [ ] Visit `/partners/apply` — wizard loads full-page on black
background, no menu, no footer
- [ ] Complete the wizard and submit — redirects to `/partners/list`
- [ ] Visit `/partners` — "Become a partner" modal still opens normally
2026-06-04 12:30:26 +00:00
Rashad Karanouh d0e0e27035 [Website] Partner application wizard + logic-function handover (#21039)
## Summary

Replaces the single-screen partner-application modal with a **4-step
wizard** on the public form, and points the route's upstream at the new
`submit-partner-application` HTTP logic function in the twenty-partners
SDK app.

After design review, the Expertise step landed on the validated
**Category + Skills** model: a small set of *stable* macro categories
the partner operates in, plus a *free, semi-structured* Skills field for
the concrete things that differentiate them (React, SAP, Shopify, …).

Companion PR (partners-app side): #21040

## ⚠️ Deployment notes

Before this can ship to prod, the website worker needs a new env var:

- **Add `PARTNER_APPLICATION_SECRET`** to the deploy config at
https://github.com/twentyhq/twenty-infra/tree/main/cloudflare/website.
Without it the route returns `503` ("Partner application endpoint is not
configured.").
- The value must **match** the `PARTNER_APPLICATION_SECRET` workspace
variable set in the partners workspace UI (Settings → Apps → Twenty
Partners → Variables) — that's how the handler authenticates the
incoming `X-Application-Secret` header.
- `PARTNER_APPLICATION_WEBHOOK_URL` also needs repointing from the TFT
webhook to the logic-function URL
(`https://partner.twenty.com/s/partner-applications` or equivalent) at
the same time.

## Wizard

- 4 steps inside `Modal.Root`: **Identity → Profile → Expertise →
Commercials**. Step-dot indicator, per-step required-field gating, reset
on close. The big serif hero shows **only on step 1**; later steps use
the compact `STEP n OF 4 · NAME` strip to reclaim vertical space.
- **Profile** captures Type of team (Solo/Agency), LinkedIn, City,
Country, Languages. Country uses the searchable Select (placeholder-only
label).
- **Expertise = Category + Skills + Notes:**
- **Category** — multi-select cards over 5 macro categories (`ADVISORY`,
`SOLUTIONING`, `DEVELOPMENT`, `HOSTING`, `SUPPORT`), each with a
one-line description + examples. (Replaces the old draft `partnerScope`
enum; the backend keeps the field name — see #21040.)
- **Skills** — free tag input with a clickable suggestion row + keyboard
autocomplete (↑/↓/Enter/Esc) and "add your own". Empty by default.
- **Notes** — one free textarea (merges the former `workspaceUrl` +
`customerReferences`), reviewed manually.
- `deploymentExpertise` removed from the form (covered by the Hosting
category).
- **In-modal success view** on submit ("Thanks, / we'll be in touch!")
with a Close button — replaces the old silent close.
- Removes the partners-page "Which partner program is right for you?"
three-cards section.

## Design-system primitives

- **`Form.Select`** — searchable popup whose dropdown is **portaled to
`<body>`** (fixed, anchored to the trigger, flips up, height-capped) so
the modal's `overflow`/`transform` can't clip it; pointer events are
stopped so clicking inside it doesn't dismiss the dialog.
- **`Form.TagInput`** — optional `suggestions` prop adds the suggestion
row + autocomplete menu (used by Skills); behaviour unchanged when no
suggestions are passed.
- **`CategoryCardSelect`** — compact multi-select cards.
- `Form.MultiSelect`, `Form.Currency`.

## Validation & payload

- **Single validation source:** client and server share Zod field
schemas (`partner-application-field-schemas.ts`). The reducer validates
via those instead of hand-rolled regexes, so client and server agree by
construction (e.g. both reject non-TLD URLs).
- **Typed request body:** `buildPartnerApplicationRequestBody(state)`
returns a typed `PartnerApplicationRequest` (unit-tested);
`handleSubmit` just serializes it.
- Payload is camelCase matching the logic-function input;
`applicationNotes` replaces `workspaceUrl`/`customerReferences`.
- Auth: the upstream call carries an `X-Application-Secret` header
backed by `PARTNER_APPLICATION_SECRET` (handler-enforced — the SDK's
`isAuthRequired` only accepts user-session JWTs, not workspace API
keys). The webhook-URL env uses `z.url()` (not `z.httpUrl()`) so
`http://localhost:2020/...` dev destinations parse.

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

## Tests

- **62 passing** across reducer, Zod schema, route, the new
payload-builder suite, and Form helper suites. `npx tsc` clean, `nx
lint:diff-with-main` clean, Lingui catalogs regenerated (French slots
are a follow-up).

## Test plan

- [ ] `/partners` → "Become a partner" → wizard opens on Step 1 (full
hero)
- [ ] Identity: name / work email / company → Next
- [ ] Profile: pick **Type of team**; search country ("fra" → France);
pick languages → Next (compact header from here on)
- [ ] Expertise: select 1+ **Category** cards; add **Skills** (click a
suggestion, type one + Enter, drive the ↑/↓ autocomplete); optionally
fill **Notes**
- [ ] Country dropdown opens without being clipped by the modal, and
clicking inside it does **not** close the wizard
- [ ] Commercials → Submit → **in-modal "Thanks, we'll be in touch!"**;
Network shows POST `/api/partner-application` `200`
- [ ] Partner record lands with the chosen categories in `partnerScope`,
plus `skills`, `applicationNotes`, `slug` from company, `reviewed:
false`, `partnerTier: 'NEW'`
- [ ] Re-submit same email + different city → Partner updates;
`validationStage`/`reviewed`/`partnerTier` preserved
- [ ] Back/Next preserves entered values; Reset on close; mobile
single-column / chips wrap

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 10:23:31 +00:00
Félix Malfait 658bdf3e57 chore(website): rename twenty-website-new → twenty-website (#20745)
## Summary
Follow-up to the Cloudflare/OpenNext migration (#20741). Now that the
legacy `twenty-website` package was already removed in #20270, the
`-new` suffix on the marketing site package is no longer meaningful.

## What changes
- **Directory rename**: `git mv packages/twenty-website-new
packages/twenty-website` (1213 files moved, no content change)
- **Package + nx config**: `package.json` and `project.json` name fields
updated, `sourceRoot` repointed
- **Source refs**: `load-local-articles.ts` and
`load-local-release-notes.ts` had a hardcoded `'twenty-website-new'`
segment in their monorepo-root fallback path;
`app/[locale]/releases/page.tsx` had display strings showing where to
add content
- **External refs**: root `package.json` workspaces, root `CLAUDE.md` /
`README.md`, `twenty-sdk` + `create-twenty-app` READMEs,
`.vscode/twenty.code-workspace`, `.cursor/rules/changelog-process.mdc`,
Crowdin config + the three `website-i18n-*` CI workflows +
`ci-website.yaml`
- **Docker cleanup**:
`packages/twenty-docker/twenty-website-new/Dockerfile` deleted; the two
Makefile targets (`prod-website-new-build` / `prod-website-new-run`)
that referenced it removed — EKS deploy was retired in the Cloudflare
migration
- **`yarn.lock`** regenerated against the new workspace path

## What's deliberately not in this PR
The dev hostname `website-new.twenty-main.com` in `wrangler.jsonc` stays
for now. Migrating it to `website.twenty-main.com` needs coordinated DNS
deletion (current CNAME points at the legacy Docusaurus NLB and serves
503s) and removal of the matching legacy `website` Helm chart in
`twenty-infra`. Flagged as a separate cleanup.

Companion infra PR: https://github.com/twentyhq/twenty-infra/pull/682
(workflow paths + Terraform ECR + docs)

## Test plan
- [x] `yarn install --immutable` resolves clean against the new path
- [x] `npx nx typecheck twenty-website` passes
- [x] `npx nx lint twenty-website` passes
- [ ] CI on this PR confirms the same on a fresh checkout
- [ ] After merge: trigger `Deploy Website` workflow against
`environment=dev` to confirm the renamed working-directory deploys
correctly
2026-05-19 23:42:09 +02:00
Abdullah. 59107b5b23 Remove twenty-website package. (#20270) 2026-05-05 12:45:02 +02:00
Marie c1da7be6d7 Billing for self-hosts (#18075)
## Summary

Implements enterprise licensing and per-seat billing for self-hosted
environments, with Stripe as the single source of truth for subscription
data.

### Components

- **twenty-website** hosts the private key to sign `ENTERPRISE_KEY` and
`ENTERPRISE_VALIDITY_TOKEN`. It communicates with Stripe to emit the
daily `ENTERPRISE_VALIDITY_TOKEN` if the subscription is active, based
on the user's Stripe subscription ID stored in `ENTERPRISE_KEY`.
- **Stripe** is the single source of truth for subscription data
(status, seats, billing).
- **The client** (twenty-server + DB + workers) saves `ENTERPRISE_KEY`
in the `keyValuePair` table (or `.env` if
`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false) and the daily-renewed
`ENTERPRISE_VALIDITY_TOKEN` in the `appToken` table.
`ENTERPRISE_VALIDITY_TOKEN` is verified client-side using a public key
to grant access to enterprise features (RLS, SSO, audit logs, etc.).

### Flow

1. When requesting an upgrade to an enterprise plan (from **Enterprise**
in settings), the user is shown a modal to choose monthly/yearly
billing, then redirected to Stripe to enter payment details. After
checkout, they land on twenty-website where they are exposed to their
`ENTERPRISE_KEY`, which they paste in the UI. It is saved in the
`keyValuePair` table. On activation, a first `ENTERPRISE_VALIDITY_TOKEN`
with 30-day validity is stored in the `appToken` table.

2. **Every day**, a cron job runs and does two things:
- **Refreshes the validity token**: communicates with twenty-website to
get a new `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity if the Stripe
subscription is still active. If the subscription is in cancellation,
the emitted token has a validity equal to the cancellation date. If it's
no longer valid, the token is not replaced. The cron only needs to run
every 30 days in practice, but runs daily so it's resilient to
occasional failures.
- **Reports seat count**: counts active (non-soft-deleted)
`UserWorkspace` entries and sends the count to twenty-website, which
updates the Stripe subscription quantity with proration. Seats are also
reported on first activation. If the subscription is canceled or
scheduled for cancellation, the seat update is skipped.

3. `ENTERPRISE_VALIDITY_TOKEN` is verified server-side via a public key
to grant access to enterprise features.

### Key concepts

Three distinct checks are exposed as GraphQL fields on `Workspace`:

| Field | Meaning |
|---|---|
| `hasValidEnterpriseKey` | Has any valid enterprise key (signed JWT
**or** legacy plain string) |
| `hasValidSignedEnterpriseKey` | `ENTERPRISE_KEY` is a properly signed
JWT (billing portal makes sense) |
| `hasValidEnterpriseValidityToken` | `ENTERPRISE_VALIDITY_TOKEN` is
present and not expired (expiration depends on signed token payload, not
on "expiresAt" on appToken table which is only indicative) |

Feature access is gated by `isValid()` =
`hasValidEnterpriseValidityToken || hasValidEnterpriseKey` (to support
both new and legacy keys during transition). After transition isValid()
= hasValidEnterpriseValidityToken

### Frontend states

The Enterprise settings page handles multiple states:
- **No key**: show "Get Enterprise" with checkout modal
- **Orphaned validity token** (token valid but no signed key): prompt
user to set a valid enterprise key
- **Active/trialing but no validity token**: show subscription status
with a "Reload validity token" action
- **Active/trialing**: show full subscription info, billing portal
access, cancel option
- **Cancellation scheduled**: show cancellation date, billing portal
- **Canceled**: show billing history link and option to start a new
subscription
- **Past due / Incomplete**: prompt to update payment or restart

### Temporary retro-compatibility: legacy plain-text keys

Previously, enterprise features were gated by a simple check: any
non-empty string in `ENTERPRISE_KEY` granted access. With this PR, we
transition to a controlled system relying on signed JWTs.

To avoid breaking existing self-hosted users:
- **Legacy plain-text keys still grant access** to enterprise features.
`hasValidEnterpriseKey` returns `true` for both signed JWTs and plain
strings, and `isValid()` checks `hasValidEnterpriseKey` as a fallback
when no validity token is present.
- **A deprecation banner** is shown at the top of the app when
`hasValidEnterpriseKey` is `true` but `hasValidSignedEnterpriseKey` is
`false`, informing the user that their key format is deprecated and they
should activate a new signed key.
- **No billing portal or subscription management** is available for
legacy keys since there is no Stripe subscription to manage.

This retro-compatibility will be removed in a future version. At that
point, `isValid()` will only check `hasValidEnterpriseValidityToken`.

### Edge cases

- **Air-gapped / production environments**: for self-hosted clients that
block external traffic (or for our own production), provide a long-lived
`ENTERPRISE_VALIDITY_TOKEN` (e.g. 99 years) directly in the `appToken`
table, with no `ENTERPRISE_KEY`. The daily cron will skip the refresh
(no enterprise key to authenticate with), but the pre-seeded validity
token will be used to grant feature access. No billing or seat reporting
occurs in this mode.
- **`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false**: if the user tries to
activate an enterprise key but DB config writes are disabled, the
backend returns a clear error asking them to add `ENTERPRISE_KEY` to
their `.env` file manually.
- **Canceled subscriptions**: the `/seats` endpoint skips Stripe updates
for canceled or cancellation-scheduled subscriptions to avoid Stripe API
errors.

### How to test
- launch twenty-website on a different url (eg localhost:1002)
- add ENTERPRISE_API_URL=http://localhost:3002/api/enterprise (or else)
in your server .env
- ask me for twenty-website's .env file content (STRIPE_SECRET_KEY;
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID;STRIPE_ENTERPRISE_YEARLY_PRICE_ID;
ENTERPRISE_JWT_PRIVATE_KEY; ENTERPRISE_JWT_PUBLIC_KEY;
NEXT_PUBLIC_WEBSITE_URL)
- visit Admin panel / enterprise
2026-03-12 15:07:53 +01:00
Félix Malfait 033bedde0a Upgrade NX (#13758)
Upgrade from NX18 to NX21
2025-08-08 10:38:12 +02:00
Baptiste Devessier ce42928143 Fix keystatic github redirect_uri by relying on x-forwarded-* headers (#10862)
Fix suggested by
https://github.com/Thinkmill/keystatic/issues/1022#issuecomment-2009029315.
2025-03-13 18:18:48 +01:00
Baptiste Devessier 2c465bd42e Integrate Keystatic to edit twenty.com content (#10709)
This PR introduces Keystatic to let us edit twenty.com's content with a
CMS. For now, we'll focus on creating release notes through Keystatic as
it uses quite simple Markdown. Other types of content will need some
refactoring to work with Keystatic.


https://github.com/user-attachments/assets/e9f85bbf-daff-4b41-bc97-d1baf63758b2

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2025-03-07 07:59:06 +01:00
Ady Beraud 5ad59b5845 Create congratulations bot (#5404)
- Created congratulations bot :
<img width="939" alt="Screenshot 2024-05-14 at 12 47 13"
src="https://github.com/twentyhq/twenty/assets/102751374/5138515f-fe4d-4c6d-9c7a-0240accbfca9">

- Modified OG image

- Added png extension to OG image route

To be noted: The bot will not work until the new API route is not
deployed. Please check OG image with Cloudflare cache.

---------

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
2024-05-21 22:56:25 +02:00
Ady Beraud 43cd8cc451 Modified URLs and added button to share on LinkedIn (#5306)
- Removed the env variable and added the current URL in contributor's
page
- Added button to share on LinkedIn on contributor's profile
- Fixed absolute image URL for release API

---------

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
2024-05-07 08:59:03 +02:00
Ady Beraud b438fc2754 Fix github stars endpoint (#5301)
- Encapsulated GitHub star response in an object
- Fixed rounding of Github stars to align with Github convention
- Fixed CORS issue so that endpoint can be called from twenty.com and
app.twenty.com

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
2024-05-07 08:35:54 +02:00
Ady Beraud 20670695d6 Added OG Image (#5251)
- Added dynamic OG Image to share and download in contributors page

<img width="1176" alt="Screenshot 2024-05-02 at 16 24 00"
src="https://github.com/twentyhq/twenty/assets/102751374/0579454b-ccc7-46ba-9875-52458f06ee82">

- Added dynamic metadata 

- Added design to contributor page

- Added a NEXT_PUBLIC_HOST_URL in the .env file

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
2024-05-03 16:38:41 +02:00
Ady Beraud df5cb9a904 Smart changelog (#5205)
Added a smart Changelog :

- Publish the Changelog before the app release. If the release has not
yet been pushed to production, do not display it.
- When the app release is done, make the Changelog available with the
correct date.
- If the Changelog writing is delayed because the release has already
been made, publish it immediately.
- Display everything locally to be able to iterate on the changelog and
have a preview

Added an endpoint for the Changelog

---------

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2024-05-01 08:35:11 +02:00
Ady Beraud 0a7f82333b Make Github stars dynamic and improve database init (#5000)
I extracted the init database logic into its own file. 
You can now run it with yarn database:init.
Added database entry for GitHub stars. 

Do you want me to remove the init route or is it used for something else
?

---------

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2024-04-24 09:44:44 +02:00
Félix Malfait 3d5a364e29 Marketing website improvements (#3169)
* Website improvement

* Improve website design

* Start writing script for user guide

* Begin adding user guide
2023-12-27 16:14:42 +01:00
Félix Malfait 5ef5bbdc4d Marketing website POC (#3139)
First website POC
2023-12-23 10:08:55 +01:00