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.
This commit is contained in:
Félix Malfait
2026-07-30 18:54:30 +02:00
committed by GitHub
parent 830404b215
commit c25c0f4698
21 changed files with 1776 additions and 26 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

@@ -0,0 +1,31 @@
import { getCommunityStats } from '@/platform/community';
import {
getRouteI18n,
type LocaleRouteParams,
} from '@/platform/i18n/get-route-i18n';
import { buildRouteMetadata } from '@/platform/seo';
import { ComparePage } from '@/sections/compare/ComparePage';
import { COMPARISONS } from '@/sections/compare/compare-data';
import { Menu } from '@/sections/menu';
export const generateMetadata = buildRouteMetadata('comparePricingHubspot');
export default async function CompareHubspotPricingPage({
params,
}: {
params: Promise<LocaleRouteParams>;
}) {
const [, communityStats] = await Promise.all([
getRouteI18n(params),
getCommunityStats(),
]);
return (
<>
<Menu communityStats={communityStats} scheme="muted" />
<main>
<ComparePage comparison={COMPARISONS.hubspot} />
</main>
</>
);
}
@@ -0,0 +1,31 @@
import { getCommunityStats } from '@/platform/community';
import {
getRouteI18n,
type LocaleRouteParams,
} from '@/platform/i18n/get-route-i18n';
import { buildRouteMetadata } from '@/platform/seo';
import { ComparePage } from '@/sections/compare/ComparePage';
import { COMPARISONS } from '@/sections/compare/compare-data';
import { Menu } from '@/sections/menu';
export const generateMetadata = buildRouteMetadata('comparePricingDynamics');
export default async function CompareMicrosoftDynamicsPricingPage({
params,
}: {
params: Promise<LocaleRouteParams>;
}) {
const [, communityStats] = await Promise.all([
getRouteI18n(params),
getCommunityStats(),
]);
return (
<>
<Menu communityStats={communityStats} scheme="muted" />
<main>
<ComparePage comparison={COMPARISONS.dynamics} />
</main>
</>
);
}
@@ -0,0 +1,31 @@
import { getCommunityStats } from '@/platform/community';
import {
getRouteI18n,
type LocaleRouteParams,
} from '@/platform/i18n/get-route-i18n';
import { buildRouteMetadata } from '@/platform/seo';
import { ComparePage } from '@/sections/compare/ComparePage';
import { COMPARISONS } from '@/sections/compare/compare-data';
import { Menu } from '@/sections/menu';
export const generateMetadata = buildRouteMetadata('comparePricingPipedrive');
export default async function ComparePipedrivePricingPage({
params,
}: {
params: Promise<LocaleRouteParams>;
}) {
const [, communityStats] = await Promise.all([
getRouteI18n(params),
getCommunityStats(),
]);
return (
<>
<Menu communityStats={communityStats} scheme="muted" />
<main>
<ComparePage comparison={COMPARISONS.pipedrive} />
</main>
</>
);
}
@@ -0,0 +1,31 @@
import { getCommunityStats } from '@/platform/community';
import {
getRouteI18n,
type LocaleRouteParams,
} from '@/platform/i18n/get-route-i18n';
import { buildRouteMetadata } from '@/platform/seo';
import { ComparePage } from '@/sections/compare/ComparePage';
import { COMPARISONS } from '@/sections/compare/compare-data';
import { Menu } from '@/sections/menu';
export const generateMetadata = buildRouteMetadata('comparePricingSalesforce');
export default async function CompareSalesforcePricingPage({
params,
}: {
params: Promise<LocaleRouteParams>;
}) {
const [, communityStats] = await Promise.all([
getRouteI18n(params),
getCommunityStats(),
]);
return (
<>
<Menu communityStats={communityStats} scheme="muted" />
<main>
<ComparePage comparison={COMPARISONS.salesforce} />
</main>
</>
);
}
@@ -0,0 +1,31 @@
import { getCommunityStats } from '@/platform/community';
import {
getRouteI18n,
type LocaleRouteParams,
} from '@/platform/i18n/get-route-i18n';
import { buildRouteMetadata } from '@/platform/seo';
import { ComparePage } from '@/sections/compare/ComparePage';
import { COMPARISONS } from '@/sections/compare/compare-data';
import { Menu } from '@/sections/menu';
export const generateMetadata = buildRouteMetadata('comparePricingSap');
export default async function CompareSapPricingPage({
params,
}: {
params: Promise<LocaleRouteParams>;
}) {
const [, communityStats] = await Promise.all([
getRouteI18n(params),
getCommunityStats(),
]);
return (
<>
<Menu communityStats={communityStats} scheme="muted" />
<main>
<ComparePage comparison={COMPARISONS.sap} />
</main>
</>
);
}
@@ -39,6 +39,51 @@ export const STATIC_WEBSITE_ROUTES: readonly WebsiteRoute[] = [
priority: 0.9,
title: msg`Twenty CRM Pricing — Plans from $9 per User per Month`,
},
{
changeFrequency: 'monthly',
description: msg`HubSpot gates custom objects behind its $150/seat Enterprise tier. See the full price comparison, with sources, and what Twenty includes from $9 per user.`,
id: 'comparePricingHubspot',
indexed: true,
path: '/compare-pricing/hubspot',
priority: 0.7,
title: msg`Twenty vs HubSpot Pricing — The Real Cost Compared`,
},
{
changeFrequency: 'monthly',
description: msg`Sales Cloud Enterprise lists at $175/user/month before support, sandboxes, and AI add-ons. Compare the real cost with Twenty, from $9 per user.`,
id: 'comparePricingSalesforce',
indexed: true,
path: '/compare-pricing/salesforce',
priority: 0.7,
title: msg`Twenty vs Salesforce Pricing — The Real Cost Compared`,
},
{
changeFrequency: 'monthly',
description: msg`SAP Sales Cloud lists at $134/user/month before a year-one implementation project that often exceeds the licenses. Compare with Twenty, from $9 per user.`,
id: 'comparePricingSap',
indexed: true,
path: '/compare-pricing/sap-sales-cloud',
priority: 0.7,
title: msg`Twenty vs SAP Sales Cloud Pricing — The Real Cost Compared`,
},
{
changeFrequency: 'monthly',
description: msg`Pipedrive caps automations per company and keeps SSO for its top tier. Compare real per-seat costs with Twenty, from $9 per user with no caps.`,
id: 'comparePricingPipedrive',
indexed: true,
path: '/compare-pricing/pipedrive',
priority: 0.7,
title: msg`Twenty vs Pipedrive Pricing — The Real Cost Compared`,
},
{
changeFrequency: 'monthly',
description: msg`Dynamics 365 Sales runs $105/user/month plus $40/GB database storage. See the full comparison with Twenty, from $9 per user, open source.`,
id: 'comparePricingDynamics',
indexed: true,
path: '/compare-pricing/microsoft-dynamics',
priority: 0.7,
title: msg`Twenty vs Microsoft Dynamics Pricing — The Real Cost Compared`,
},
{
changeFrequency: 'monthly',
description: msg`Find a certified Twenty partner to migrate, customise, and operate your open source CRM, or join the ecosystem and grow your practice with us.`,
@@ -3,6 +3,11 @@ import { type MetadataRoute } from 'next';
export type WebsiteRouteId =
| 'apps'
| 'comparePricingDynamics'
| 'comparePricingHubspot'
| 'comparePricingPipedrive'
| 'comparePricingSalesforce'
| 'comparePricingSap'
| 'customers'
| 'enterpriseActivate'
| 'halftone'
@@ -0,0 +1,209 @@
import { msg } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { INFORMATIVE_MARKS } from '@/icons';
import { getServerI18n } from '@/platform/i18n/get-server-i18n';
import {
color,
FONT_WEIGHT,
fontFamily,
fontSize,
mediaUp,
semanticColor,
spacing,
} from '@/tokens';
import { ExternalLink } from '@/ui';
import { type CompetitorComparison } from './compare-data';
const CheckMark = INFORMATIVE_MARKS.check;
const REPORT_INACCURACY_URL =
'https://github.com/twentyhq/twenty/issues/new/choose';
// On narrow viewports the three-column grid scrolls horizontally instead of
// squeezing the price cells.
const TableScroller = styled.div`
overflow-x: auto;
width: 100%;
`;
const TableScope = styled.div`
color: ${semanticColor.ink};
min-width: 640px;
width: 100%;
`;
const GridRow = styled.div`
display: grid;
grid-template-columns: 1.2fr 1fr 1fr;
width: 100%;
`;
const HeadCell = styled.div`
border-bottom: 1px solid ${semanticColor.line};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(5.5)};
font-weight: ${FONT_WEIGHT.medium};
line-height: ${spacing(7)};
padding: ${spacing(4)};
&[data-tier] {
border-left: 1px solid ${semanticColor.line};
}
`;
const FeatureCell = styled.div`
border-bottom: 1px solid ${semanticColor.line};
display: flex;
flex-direction: column;
gap: ${spacing(1)};
justify-content: center;
min-height: ${spacing(18)};
padding: ${spacing(4)};
`;
const FeatureName = styled.span`
font-family: ${fontFamily('sans')};
font-size: ${fontSize(4)};
font-weight: ${FONT_WEIGHT.medium};
`;
const FeatureDescription = styled.span`
color: ${semanticColor.inkMuted};
display: none;
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3.5)};
${mediaUp('md')} {
display: block;
}
`;
const ValueCell = styled.div`
border-bottom: 1px solid ${semanticColor.line};
border-left: 1px solid ${semanticColor.line};
display: flex;
flex-direction: column;
gap: ${spacing(1)};
justify-content: center;
min-height: ${spacing(18)};
padding: ${spacing(4)};
`;
const CompetitorPrice = styled.span`
font-family: ${fontFamily('sans')};
font-size: ${fontSize(4)};
font-weight: ${FONT_WEIGHT.medium};
`;
const CompetitorDetail = styled.span`
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3.5)};
`;
const SourceLink = styled(ExternalLink)`
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3)};
text-decoration: underline;
text-underline-offset: 2px;
width: fit-content;
&:hover {
color: ${semanticColor.ink};
}
`;
const IncludedRow = styled.div`
align-items: center;
column-gap: ${spacing(2.5)};
display: flex;
`;
const IncludedText = styled.span`
font-family: ${fontFamily('sans')};
font-size: ${fontSize(4)};
`;
const SourceNote = styled.p`
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3.5)};
line-height: ${spacing(5.5)};
margin-top: ${spacing(6)};
max-width: 640px;
`;
const ReportLink = styled(ExternalLink)`
color: ${semanticColor.inkMuted};
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
color: ${semanticColor.ink};
}
`;
export function CompareCostTable({
comparison,
}: {
comparison: CompetitorComparison;
}) {
const i18n = getServerI18n();
return (
<div>
<TableScroller>
<TableScope>
<GridRow>
<HeadCell>{i18n._(msg`Feature`)}</HeadCell>
<HeadCell data-tier>
{i18n._(comparison.competitorColumnLabel)}
</HeadCell>
<HeadCell data-tier>{i18n._(msg`On Twenty`)}</HeadCell>
</GridRow>
{comparison.rows.map((row) => (
<GridRow key={i18n._(row.feature)}>
<FeatureCell>
<FeatureName>{i18n._(row.feature)}</FeatureName>
<FeatureDescription>
{i18n._(row.description)}
</FeatureDescription>
</FeatureCell>
<ValueCell>
<CompetitorPrice>
{i18n._(row.competitor.price)}
</CompetitorPrice>
<CompetitorDetail>
{i18n._(row.competitor.detail)}
</CompetitorDetail>
{row.competitor.sourceUrl !== undefined ? (
<SourceLink
href={row.competitor.sourceUrl}
rel="nofollow noreferrer"
>
{i18n._(msg`source`)}
</SourceLink>
) : null}
</ValueCell>
<ValueCell>
<IncludedRow>
<CheckMark color={color('blue')} sizePx={16} />
<IncludedText>{i18n._(row.twenty.detail)}</IncludedText>
</IncludedRow>
</ValueCell>
</GridRow>
))}
</TableScope>
</TableScroller>
<SourceNote>
{i18n._(comparison.sourceNote)}{' '}
<ReportLink href={REPORT_INACCURACY_URL}>
{i18n._(msg`Spotted an inaccuracy? Tell us and we will fix it.`)}
</ReportLink>
</SourceNote>
</div>
);
}
@@ -0,0 +1,137 @@
import { msg } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { getServerI18n } from '@/platform/i18n/get-server-i18n';
import { GRADIENT, mediaUp, spacing } from '@/tokens';
import {
Body,
Button,
EngagementBand,
Eyebrow,
Heading,
HeadingPair,
SectionShell,
} from '@/ui';
import { CompareCostTable } from './CompareCostTable';
import { CompareReceipt } from './CompareReceipt';
import { type CompetitorComparison } from './compare-data';
const GradientBackdrop = styled.div`
background: ${GRADIENT.heroGlow};
inset: 0 -20%;
position: absolute;
`;
const HeroStack = styled.div`
align-items: center;
display: flex;
flex-direction: column;
margin-inline: auto;
text-align: center;
width: 100%;
`;
const HeadingMeasure = styled.div`
white-space: pre-line;
${mediaUp('md')} {
max-width: 720px;
}
`;
const IntroMeasure = styled.div`
white-space: pre-line;
${mediaUp('md')} {
max-width: 560px;
}
`;
const CtaRow = styled.div`
display: flex;
gap: ${spacing(3)};
justify-content: center;
margin-top: ${spacing(8)};
`;
const SectionHeading = styled.div`
margin-bottom: ${spacing(8)};
text-align: center;
`;
export function ComparePage({
comparison,
}: {
comparison: CompetitorComparison;
}) {
const i18n = getServerI18n();
return (
<>
<SectionShell
background={<GradientBackdrop />}
rhythm="hero"
scheme="muted"
>
<HeroStack>
<HeadingPair>
<Eyebrow>{i18n._(comparison.eyebrow)}</Eyebrow>
<HeadingMeasure>
<Heading as="h1" size="lg" weight="light">
{i18n._(comparison.heading)}
</Heading>
</HeadingMeasure>
<IntroMeasure>
<Body muted size="sm">
{i18n._(comparison.intro)}
</Body>
</IntroMeasure>
</HeadingPair>
<CtaRow>
<Button
href="https://app.twenty.com/welcome"
label={i18n._(msg`Start for free`)}
variant="filled"
/>
<Button
href="/pricing"
label={i18n._(msg`See Twenty pricing`)}
variant="outlined"
/>
</CtaRow>
</HeroStack>
</SectionShell>
<SectionShell rhythm="section" scheme="dark">
<SectionHeading>
<Heading as="h2" size="sm" weight="light">
{i18n._(comparison.tableTitle)}
</Heading>
</SectionHeading>
<CompareCostTable comparison={comparison} />
</SectionShell>
<SectionShell rhythm="section" scheme="muted">
<SectionHeading>
<Heading as="h2" size="sm" weight="light">
{i18n._(msg`The bill, side by side`)}
</Heading>
</SectionHeading>
<CompareReceipt comparison={comparison} />
</SectionShell>
<EngagementBand
actions={
<Button
href="https://app.twenty.com/welcome"
label={i18n._(msg`Get started`)}
variant="filled"
/>
}
body={i18n._(comparison.migrationLine)}
heading={i18n._(msg`Run the comparison on your own data`)}
/>
</>
);
}
@@ -0,0 +1,316 @@
import { msg } from '@lingui/core/macro';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
import { getServerI18n } from '@/platform/i18n/get-server-i18n';
import {
color,
FONT_WEIGHT,
fontFamily,
fontSize,
mediaUp,
radius,
semanticColor,
spacing,
} from '@/tokens';
import { Body, Button, Eyebrow, Heading } from '@/ui';
import {
type CompareReceiptLine,
type CompetitorComparison,
} from './compare-data';
const TWENTY_CARD_ICON = {
alt: 'Twenty Organization plan icon',
src: '/images/pricing/plans/organization-icon.webp',
widthPx: 80,
};
const ReceiptScope = styled.div`
display: flex;
flex-direction: column;
gap: ${spacing(6)};
width: 100%;
`;
const Scenario = styled.p`
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(4)};
text-align: center;
`;
const CardsGrid = styled.div`
display: grid;
gap: ${spacing(4)};
grid-template-columns: 1fr;
width: 100%;
${mediaUp('md')} {
gap: ${spacing(6)};
grid-template-columns: 1fr 1fr;
}
`;
// Mirrors the pricing page's PlanCard shell so both pages read as one
// component family.
const CardShell = styled.div`
background-color: ${color('white')};
border: 1px solid transparent;
border-radius: ${radius(2)};
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
padding: ${spacing(4)};
position: relative;
z-index: 1;
& > * + * {
margin-top: ${spacing(4)};
}
`;
const CardHeader = styled.div`
align-items: flex-start;
display: flex;
flex-shrink: 0;
gap: ${spacing(3)};
justify-content: space-between;
`;
const CardHeaderInfo = styled.div`
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
overflow: hidden;
& > * + * {
margin-top: ${spacing(4)};
}
`;
const titleClassName = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const PriceLine = styled.div`
align-items: baseline;
column-gap: ${spacing(1)};
display: flex;
flex-wrap: wrap;
min-width: 0;
`;
const priceSuffixClassName = css`
color: ${color('black-60')};
display: block;
min-width: 0;
`;
// Clones the billing toggle's discount badge so the savings chip is the
// same component language as the pricing page's "-25%".
const SavingsBadge = styled.span`
align-items: center;
align-self: center;
background-color: ${color('blue')};
border-radius: ${radius(12)};
color: ${color('white')};
display: inline-flex;
font-family: ${fontFamily('sans')};
font-size: ${fontSize(2.5)};
font-weight: ${FONT_WEIGHT.medium};
height: 24px;
justify-content: center;
margin-left: ${spacing(1)};
padding-inline: ${spacing(1.5)};
white-space: nowrap;
`;
const CardRule = styled.div`
border-top: 1px dotted ${semanticColor.divider};
flex-shrink: 0;
height: 0;
width: 100%;
`;
const CardIcon = styled.div`
border-radius: ${radius(2)};
flex-shrink: 0;
height: 80px;
margin-left: auto;
overflow: hidden;
position: relative;
width: ${TWENTY_CARD_ICON.widthPx}px;
img {
object-fit: contain;
object-position: center right;
}
`;
const LinesList = styled.div`
display: flex;
flex: 1;
flex-direction: column;
gap: ${spacing(3)};
`;
const LineRow = styled.div`
align-items: baseline;
display: flex;
font-family: ${fontFamily('sans')};
font-size: ${fontSize(4)};
gap: ${spacing(3)};
justify-content: space-between;
line-height: ${spacing(5.5)};
`;
const LineLabel = styled.span`
color: ${semanticColor.inkMuted};
`;
const LineAmount = styled.span`
white-space: nowrap;
`;
const PerUserLine = styled.p`
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3.5)};
`;
const CtaRow = styled.div`
flex-shrink: 0;
margin-top: auto;
padding-top: ${spacing(4)};
width: 100%;
> * {
display: flex;
width: 100%;
}
`;
const FairPlayBlock = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: ${spacing(3)};
margin-inline: auto;
margin-top: ${spacing(6)};
max-width: 560px;
text-align: center;
`;
function ReceiptLines({ lines }: { lines: CompareReceiptLine[] }) {
const i18n = getServerI18n();
return (
<LinesList>
{lines.map((line) => (
<LineRow key={i18n._(line.label)}>
<LineLabel>{i18n._(line.label)}</LineLabel>
<LineAmount>{i18n._(line.amount)}</LineAmount>
</LineRow>
))}
</LinesList>
);
}
export function CompareReceipt({
comparison,
}: {
comparison: CompetitorComparison;
}) {
const i18n = getServerI18n();
const { receipt } = comparison;
return (
<ReceiptScope>
<Scenario>{i18n._(receipt.scenario)}</Scenario>
<CardsGrid>
<CardShell>
<CardHeader>
<CardHeaderInfo>
<Heading
as="h3"
className={titleClassName}
family="sans"
size="xs"
weight="light"
>
{comparison.competitor}
</Heading>
<PriceLine>
<Heading as="h4" family="sans" size="sm" weight="regular">
{i18n._(receipt.competitorTotalAmount)}
</Heading>
<Body as="span" className={priceSuffixClassName} size="sm">
{i18n._(receipt.competitorTotalSuffix)}
</Body>
</PriceLine>
</CardHeaderInfo>
</CardHeader>
<CardRule />
<ReceiptLines lines={receipt.competitorLines} />
<PerUserLine>{i18n._(receipt.competitorPerUser)}</PerUserLine>
</CardShell>
<CardShell>
<CardHeader>
<CardHeaderInfo>
<Heading
as="h3"
className={titleClassName}
family="sans"
size="xs"
weight="light"
>
Twenty
</Heading>
<PriceLine>
<Heading as="h4" family="sans" size="sm" weight="regular">
{i18n._(receipt.twentyTotalAmount)}
</Heading>
<Body as="span" className={priceSuffixClassName} size="sm">
{i18n._(receipt.twentyTotalSuffix)}
</Body>
<SavingsBadge>{i18n._(receipt.multiplier)}</SavingsBadge>
</PriceLine>
</CardHeaderInfo>
<CardIcon>
<NextImage
alt={TWENTY_CARD_ICON.alt}
fill
sizes={`${TWENTY_CARD_ICON.widthPx}px`}
src={TWENTY_CARD_ICON.src}
/>
</CardIcon>
</CardHeader>
<CardRule />
<ReceiptLines lines={receipt.twentyLines} />
<PerUserLine>{i18n._(receipt.twentyPerUser)}</PerUserLine>
<CtaRow>
<Button
href="https://app.twenty.com/welcome"
label={i18n._(msg`Start for free`)}
variant="filled"
/>
</CtaRow>
</CardShell>
</CardsGrid>
<FairPlayBlock>
<Eyebrow>{i18n._(msg`Fair play`)}</Eyebrow>
<Body muted size="sm">
{i18n._(comparison.honest)}
</Body>
</FairPlayBlock>
</ReceiptScope>
);
}
@@ -0,0 +1,604 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
// Competitor list prices, checked against each vendor's public pricing page
// on 2026-07-30 (annual billing unless stated). Keep every price cell paired
// with its source URL; re-verify before changing any number.
export type CompareCompetitorCell = {
detail: MessageDescriptor;
price: MessageDescriptor;
sourceUrl?: string;
};
export type CompareRow = {
competitor: CompareCompetitorCell;
description: MessageDescriptor;
feature: MessageDescriptor;
twenty: { detail: MessageDescriptor };
};
export type CompareReceiptLine = {
amount: MessageDescriptor;
label: MessageDescriptor;
};
export type CompetitorComparison = {
competitor: string;
competitorColumnLabel: MessageDescriptor;
eyebrow: MessageDescriptor;
heading: MessageDescriptor;
honest: MessageDescriptor;
intro: MessageDescriptor;
migrationLine: MessageDescriptor;
receipt: {
competitorLines: CompareReceiptLine[];
competitorPerUser: MessageDescriptor;
competitorTotalAmount: MessageDescriptor;
competitorTotalSuffix: MessageDescriptor;
multiplier: MessageDescriptor;
scenario: MessageDescriptor;
twentyLines: CompareReceiptLine[];
twentyPerUser: MessageDescriptor;
twentyTotalAmount: MessageDescriptor;
twentyTotalSuffix: MessageDescriptor;
};
rows: CompareRow[];
slug: string;
sourceNote: MessageDescriptor;
tableTitle: MessageDescriptor;
};
const HUBSPOT_PRICING_URL = 'https://www.hubspot.com/pricing/sales';
const SALESFORCE_PRICING_URL = 'https://www.salesforce.com/sales/pricing/';
// const ATTIO_PRICING_URL = 'https://attio.com/pricing';
const PIPEDRIVE_PRICING_URL = 'https://www.pipedrive.com/en/pricing';
const DYNAMICS_PRICING_URL =
'https://www.microsoft.com/en-us/dynamics-365/products/sales/pricing';
const HUBSPOT_COMPARISON: CompetitorComparison = {
competitor: 'HubSpot',
competitorColumnLabel: msg`On HubSpot`,
eyebrow: msg`Twenty vs HubSpot`,
heading: msg`What does\n*HubSpot* really cost?`,
intro: msg`A feature-by-feature price check, with sources.\nMost of what HubSpot gates behind tiers is simply included in Twenty.`,
migrationLine: msg`Import your data and see your own numbers. Most teams migrate from HubSpot in under a day.`,
slug: 'hubspot',
tableTitle: msg`What costs extra over there`,
rows: [
{
feature: msg`Custom objects`,
description: msg`Model deals, assets, or anything else`,
competitor: {
price: msg`$150/seat/mo`,
detail: msg`Sales Hub Enterprise only`,
sourceUrl: HUBSPOT_PRICING_URL,
},
twenty: { detail: msg`Included from Pro, $9/user` },
},
{
feature: msg`Full workflow automation`,
description: msg`Trigger actions on record changes`,
competitor: {
price: msg`$100/seat/mo`,
detail: msg`Professional tier and up`,
sourceUrl: HUBSPOT_PRICING_URL,
},
twenty: { detail: msg`Included from Pro` },
},
{
feature: msg`Teams & permission sets`,
description: msg`Control who sees and edits what`,
competitor: {
price: msg`$100/seat/mo`,
detail: msg`Professional tier and up`,
sourceUrl: HUBSPOT_PRICING_URL,
},
twenty: { detail: msg`Roles from Pro, row-level on Organization` },
},
{
feature: msg`SAML SSO`,
description: msg`Single sign-on for your team`,
competitor: {
price: msg`$150/seat/mo`,
detail: msg`Enterprise only`,
sourceUrl: HUBSPOT_PRICING_URL,
},
twenty: { detail: msg`Organization, $19/user` },
},
{
feature: msg`Sandboxes`,
description: msg`Test changes before production`,
competitor: {
price: msg`$150/seat/mo`,
detail: msg`Enterprise only`,
sourceUrl: HUBSPOT_PRICING_URL,
},
twenty: { detail: msg`Local environment included` },
},
{
feature: msg`Getting started`,
description: msg`Required onboarding on paid tiers`,
competitor: {
price: msg`$1,500 to $3,500`,
detail: msg`One-time mandatory onboarding fee`,
sourceUrl: HUBSPOT_PRICING_URL,
},
twenty: { detail: msg`None, start in minutes` },
},
],
receipt: {
scenario: msg`A 20-person sales team, billed annually`,
competitorLines: [
{
label: msg`20 × Sales Hub Enterprise ($150)`,
amount: msg`$36,000/yr`,
},
{ label: msg`Mandatory onboarding (one-time)`, amount: msg`$3,500` },
],
competitorTotalAmount: msg`$39,500`,
competitorTotalSuffix: msg`in year one`,
competitorPerUser: msg`$165 per user per month in year one`,
twentyLines: [
{ label: msg`20 × Organization ($19)`, amount: msg`$4,560/yr` },
{ label: msg`Onboarding`, amount: msg`$0` },
],
twentyTotalAmount: msg`$4,560`,
twentyTotalSuffix: msg`in year one`,
twentyPerUser: msg`$19 per user per month, that's it`,
multiplier: msg`≈ 8× less`,
},
honest: msg`HubSpot brings a full marketing suite and a huge agency ecosystem. If you need marketing automation more than a CRM, it is a fair choice.`,
sourceNote: msg`List prices from HubSpot's public pricing page, billed annually, checked on July 30, 2026.`,
};
const SALESFORCE_COMPARISON: CompetitorComparison = {
competitor: 'Salesforce',
competitorColumnLabel: msg`On Salesforce`,
eyebrow: msg`Twenty vs Salesforce`,
heading: msg`What does\n*Salesforce* really cost?`,
intro: msg`The list price is only the beginning.\nHere is what the invoice looks like once a real team is on it.`,
migrationLine: msg`Import your data and see your own numbers. Twenty's importer handles Salesforce exports out of the box.`,
slug: 'salesforce',
tableTitle: msg`The line items add up`,
rows: [
{
feature: msg`The CRM itself`,
description: msg`Sales Cloud, per user`,
competitor: {
price: msg`$175/user/mo`,
detail: msg`Enterprise edition`,
sourceUrl: SALESFORCE_PRICING_URL,
},
twenty: { detail: msg`$9 to $19/user, everything included` },
},
{
feature: msg`The AI edition`,
description: msg`Agents and Data Cloud bundled`,
competitor: {
price: msg`$500/user/mo`,
detail: msg`Agentforce 1 Sales edition`,
sourceUrl: SALESFORCE_PRICING_URL,
},
twenty: { detail: msg`AI agents included, usage credits` },
},
{
feature: msg`Agent conversations`,
description: msg`When customers talk to your AI`,
competitor: {
price: msg`$2/conversation`,
detail: msg`Agentforce list price`,
sourceUrl: SALESFORCE_PRICING_URL,
},
twenty: { detail: msg`Credit-based, no per-chat fee` },
},
{
feature: msg`Premier support`,
description: msg`Faster answers when it matters`,
competitor: {
price: msg`+30% of spend`,
detail: msg`Success plan uplift`,
sourceUrl: SALESFORCE_PRICING_URL,
},
twenty: { detail: msg`Priority support on Organization` },
},
{
feature: msg`Full sandbox`,
description: msg`A real staging environment`,
competitor: {
price: msg`+30% of spend`,
detail: msg`Add-on, per environment`,
sourceUrl: SALESFORCE_PRICING_URL,
},
twenty: { detail: msg`Local environment included` },
},
{
feature: msg`Implementation`,
description: msg`Getting to a working CRM`,
competitor: {
price: msg`$10k to $100k+`,
detail: msg`Typical partner project`,
},
twenty: { detail: msg`Self-serve, partners optional` },
},
],
receipt: {
scenario: msg`A 20-person sales team, billed annually`,
competitorLines: [
{
label: msg`20 × Sales Cloud Enterprise ($175)`,
amount: msg`$42,000/yr`,
},
{ label: msg`Premier support (+30%)`, amount: msg`$12,600/yr` },
],
competitorTotalAmount: msg`$54,600`,
competitorTotalSuffix: msg`per year`,
competitorPerUser: msg`$228 per user per month before add-ons`,
twentyLines: [
{ label: msg`20 × Organization ($19)`, amount: msg`$4,560/yr` },
{ label: msg`Support`, amount: msg`Included` },
],
twentyTotalAmount: msg`$4,560`,
twentyTotalSuffix: msg`per year`,
twentyPerUser: msg`$19 per user per month, that's it`,
multiplier: msg`≈ 12× less`,
},
honest: msg`Salesforce remains the deepest enterprise platform, with an ecosystem for almost everything. At a thousand seats with a dedicated admin team, it is a different conversation.`,
sourceNote: msg`List prices from Salesforce's public pricing page, billed annually, checked on July 30, 2026.`,
};
// The Attio comparison is parked for now (may return later). The data is
// kept commented so it only needs re-verification to come back.
// export const ATTIO_COMPARISON: CompetitorComparison = {
// competitor: 'Attio',
// competitorColumnLabel: msg`On Attio`,
// eyebrow: msg`Twenty vs Attio`,
// heading: msg`What does\n*Attio* really cost?`,
// intro: msg`Both are modern CRMs. One is open source.\nHere is the price of the difference.`,
// migrationLine: msg`Import your data and see your own numbers. Moving a workspace from Attio usually takes an afternoon.`,
// slug: 'attio',
// tableTitle: msg`The same features, priced differently`,
// rows: [
// {
// feature: msg`The modern CRM`,
// description: msg`Objects, pipelines, email sync`,
// competitor: {
// price: msg`$35/user/mo`,
// detail: msg`Plus plan, billed annually`,
// sourceUrl: ATTIO_PRICING_URL,
// },
// twenty: { detail: msg`$9/user on Pro` },
// },
// {
// feature: msg`The full platform`,
// description: msg`Advanced reporting and workflows`,
// competitor: {
// price: msg`$79/user/mo`,
// detail: msg`Pro plan, billed annually`,
// sourceUrl: ATTIO_PRICING_URL,
// },
// twenty: { detail: msg`Included from Pro, $9/user` },
// },
// {
// feature: msg`SAML SSO & advanced admin`,
// description: msg`Single sign-on, access control`,
// competitor: {
// price: msg`Custom pricing`,
// detail: msg`Enterprise plan only`,
// sourceUrl: ATTIO_PRICING_URL,
// },
// twenty: { detail: msg`Organization, $19/user` },
// },
// {
// feature: msg`Cost per workflow run`,
// description: msg`An automation that writes a record`,
// competitor: {
// price: msg`From ≈ 1.5¢/run`,
// detail: msg`One credit per data-writing block, $150 per 10k credits`,
// sourceUrl: ATTIO_PRICING_URL,
// },
// twenty: { detail: msg`Credit allowance included with every plan` },
// },
// {
// feature: msg`Source code`,
// description: msg`Audit it, extend it, own it`,
// competitor: {
// price: msg`Closed`,
// detail: msg`Proprietary`,
// },
// twenty: { detail: msg`Open source` },
// },
// ],
// receipt: {
// scenario: msg`A 20-person team, billed annually`,
// competitorLines: [
// { label: msg`20 × Pro ($79)`, amount: msg`$18,960/yr` },
// {
// label: msg`SSO & advanced admin`,
// amount: msg`Enterprise upgrade`,
// },
// ],
// competitorTotalAmount: msg`$18,960+`,
// competitorTotalSuffix: msg`per year`,
// competitorPerUser: msg`$79 per user per month, before Enterprise`,
// twentyLines: [
// { label: msg`20 × Organization ($19)`, amount: msg`$4,560/yr` },
// { label: msg`SSO & advanced admin`, amount: msg`Included` },
// ],
// twentyTotalAmount: msg`$4,560`,
// twentyTotalSuffix: msg`per year`,
// twentyPerUser: msg`$19 per user per month, SSO included`,
// multiplier: msg`≈ 4× less`,
// },
// honest: msg`Attio is a polished, fast product with strong built-in enrichment. If you never need to extend or own your CRM, it is a credible pick.`,
// sourceNote: msg`List prices from Attio's public pricing page, billed annually, checked on July 30, 2026.`,
// };
const PIPEDRIVE_COMPARISON: CompetitorComparison = {
competitor: 'Pipedrive',
competitorColumnLabel: msg`On Pipedrive`,
eyebrow: msg`Twenty vs Pipedrive`,
heading: msg`What does\n*Pipedrive* really cost?`,
intro: msg`A friendly price on the front page.\nThe caps show up after you sign.`,
migrationLine: msg`Import your data and see your own numbers. Pipedrive exports map cleanly onto Twenty objects.`,
slug: 'pipedrive',
tableTitle: msg`Where the caps and gates are`,
rows: [
{
feature: msg`The CRM`,
description: msg`Pipeline, contacts, email sync`,
competitor: {
price: msg`$49/user/mo`,
detail: msg`Premium plan, billed annually`,
sourceUrl: PIPEDRIVE_PRICING_URL,
},
twenty: { detail: msg`$9/user on Pro` },
},
{
feature: msg`SAML SSO & security rules`,
description: msg`Single sign-on, access policies`,
competitor: {
price: msg`$79/user/mo`,
detail: msg`Ultimate plan only`,
sourceUrl: PIPEDRIVE_PRICING_URL,
},
twenty: { detail: msg`Organization, $19/user` },
},
{
feature: msg`Active automations`,
description: msg`Company-wide, not per user`,
competitor: {
price: msg`Capped per tier`,
detail: msg`Shared limit below Ultimate`,
sourceUrl: PIPEDRIVE_PRICING_URL,
},
twenty: { detail: msg`Unlimited workflows` },
},
{
feature: msg`Custom fields`,
description: msg`Shape records to your business`,
competitor: {
price: msg`Capped per tier`,
detail: msg`Unlimited on top tiers only`,
sourceUrl: PIPEDRIVE_PRICING_URL,
},
twenty: { detail: msg`Unlimited from Pro` },
},
{
feature: msg`Custom objects`,
description: msg`Beyond deals and contacts`,
competitor: {
price: msg`Not available`,
detail: msg`Deals, contacts and products only`,
},
twenty: { detail: msg`Included from Pro` },
},
],
receipt: {
scenario: msg`A 20-person sales team, billed annually`,
competitorLines: [
{ label: msg`20 × Premium ($49)`, amount: msg`$11,760/yr` },
{
label: msg`SSO & security rules: move to Ultimate`,
amount: msg`+$7,200/yr`,
},
],
competitorTotalAmount: msg`$18,960`,
competitorTotalSuffix: msg`per year with SSO`,
competitorPerUser: msg`$79 per user per month with SSO`,
twentyLines: [
{ label: msg`20 × Organization ($19)`, amount: msg`$4,560/yr` },
{ label: msg`SSO & row-level permissions`, amount: msg`Included` },
],
twentyTotalAmount: msg`$4,560`,
twentyTotalSuffix: msg`per year`,
twentyPerUser: msg`$19 per user per month, SSO included`,
multiplier: msg`≈ 4× less`,
},
honest: msg`Pipedrive's pipeline UX is famously simple, and its entry tier is genuinely affordable for very small teams.`,
sourceNote: msg`List prices from Pipedrive's public pricing page, billed annually, checked on July 30, 2026.`,
};
const DYNAMICS_COMPARISON: CompetitorComparison = {
competitor: 'Microsoft Dynamics',
competitorColumnLabel: msg`On Dynamics 365`,
eyebrow: msg`Twenty vs Microsoft Dynamics`,
heading: msg`What does\n*Dynamics 365* really cost?`,
intro: msg`Nobody buys Dynamics, they license it.\nHere is what it costs once storage and add-ons land.`,
migrationLine: msg`Import your data and see your own numbers. Twenty's importer handles Dynamics exports without a consulting project.`,
slug: 'microsoft-dynamics',
tableTitle: msg`The licensing maze, priced`,
rows: [
{
feature: msg`The CRM`,
description: msg`Dynamics 365 Sales, per user`,
competitor: {
price: msg`$105/user/mo`,
detail: msg`Sales Enterprise edition`,
sourceUrl: DYNAMICS_PRICING_URL,
},
twenty: { detail: msg`$9 to $19/user, everything included` },
},
{
feature: msg`The AI edition`,
description: msg`Sales Premium with AI insights`,
competitor: {
price: msg`$150/user/mo`,
detail: msg`Minimum 10 users`,
sourceUrl: DYNAMICS_PRICING_URL,
},
twenty: { detail: msg`AI agents included, usage credits` },
},
{
feature: msg`Database storage`,
description: msg`Beyond the pooled entitlement`,
competitor: {
price: msg`$40/GB/month`,
detail: msg`Dataverse database add-on`,
sourceUrl: DYNAMICS_PRICING_URL,
},
twenty: { detail: msg`Included` },
},
{
feature: msg`Customization apps`,
description: msg`Extend beyond the CRM screens`,
competitor: {
price: msg`$20/user/mo`,
detail: msg`Separate Power Apps licensing`,
sourceUrl: DYNAMICS_PRICING_URL,
},
twenty: { detail: msg`Full customization included` },
},
{
feature: msg`Implementation`,
description: msg`Getting to a working CRM`,
competitor: {
price: msg`$15k to $100k+`,
detail: msg`Typical partner project`,
},
twenty: { detail: msg`Self-serve, partners optional` },
},
],
receipt: {
scenario: msg`A 20-person sales team, billed annually`,
competitorLines: [
{
label: msg`20 × Sales Enterprise ($105)`,
amount: msg`$25,200/yr`,
},
{
label: msg`Extra Dataverse storage (10 GB)`,
amount: msg`$4,800/yr`,
},
],
competitorTotalAmount: msg`$30,000`,
competitorTotalSuffix: msg`per year`,
competitorPerUser: msg`$125 per user per month`,
twentyLines: [
{ label: msg`20 × Organization ($19)`, amount: msg`$4,560/yr` },
{ label: msg`Storage`, amount: msg`Included` },
],
twentyTotalAmount: msg`$4,560`,
twentyTotalSuffix: msg`per year`,
twentyPerUser: msg`$19 per user per month, that's it`,
multiplier: msg`≈ 6× less`,
},
honest: msg`If your company lives in Microsoft 365 and already runs on the Power Platform, Dynamics integrates like nothing else.`,
sourceNote: msg`List prices from Microsoft's public Dynamics 365 pricing page, billed annually, checked on July 30, 2026.`,
};
const SAP_STORE_URL = 'https://store.sap.com';
const SAP_COMPARISON: CompetitorComparison = {
competitor: 'SAP Sales Cloud',
competitorColumnLabel: msg`On SAP`,
eyebrow: msg`Twenty vs SAP Sales Cloud`,
heading: msg`What does\n*SAP Sales Cloud* really cost?`,
intro: msg`The license is the cheap part.\nHere is what SAP Sales Cloud costs by the time it goes live.`,
migrationLine: msg`Import your data and see your own numbers. No six-month implementation project required.`,
slug: 'sap-sales-cloud',
tableTitle: msg`Enterprise software, enterprise line items`,
rows: [
{
feature: msg`The CRM itself`,
description: msg`Sales Cloud V2, per user`,
competitor: {
price: msg`$134/user/mo`,
detail: msg`SAP Store list price`,
sourceUrl: SAP_STORE_URL,
},
twenty: { detail: msg`$9 to $19/user, everything included` },
},
{
feature: msg`Contract terms`,
description: msg`How long you are committed`,
competitor: {
price: msg`Up to 36 months`,
detail: msg`Auto-renewing term contracts`,
sourceUrl: SAP_STORE_URL,
},
twenty: { detail: msg`Monthly or yearly, cancel anytime` },
},
{
feature: msg`Implementation`,
description: msg`Getting to a working CRM`,
competitor: {
price: msg`$40k to $150k+`,
detail: msg`Typical partner project, year one`,
},
twenty: { detail: msg`Self-serve, partners optional` },
},
{
feature: msg`Integration`,
description: msg`Connecting the rest of your stack`,
competitor: {
price: msg`Project-based`,
detail: msg`Middleware licensed separately`,
},
twenty: { detail: msg`REST & GraphQL APIs included` },
},
{
feature: msg`Price transparency`,
description: msg`Can you see the bill coming?`,
competitor: {
price: msg`Quote-based`,
detail: msg`Enterprise rate cards are rarely published`,
},
twenty: { detail: msg`Public pricing, this page included` },
},
],
receipt: {
scenario: msg`A 20-person sales team, billed annually`,
competitorLines: [
{
label: msg`20 × Sales Cloud V2 ($134)`,
amount: msg`$32,160/yr`,
},
{
label: msg`Implementation project (year one, typical)`,
amount: msg`$40,000+`,
},
],
competitorTotalAmount: msg`$72,160+`,
competitorTotalSuffix: msg`in year one`,
competitorPerUser: msg`$300 per user per month in year one`,
twentyLines: [
{ label: msg`20 × Organization ($19)`, amount: msg`$4,560/yr` },
{ label: msg`Implementation`, amount: msg`$0` },
],
twentyTotalAmount: msg`$4,560`,
twentyTotalSuffix: msg`in year one`,
twentyPerUser: msg`$19 per user per month, that's it`,
multiplier: msg`≈ 15× less`,
},
honest: msg`If your business already runs on S/4HANA, Sales Cloud speaks its language natively and handles global-enterprise compliance at serious scale.`,
sourceNote: msg`License list price from the SAP Store, billed annually, checked on July 30, 2026. Implementation figures are typical partner-project ranges, not SAP list prices.`,
};
export const COMPARISONS = {
dynamics: DYNAMICS_COMPARISON,
hubspot: HUBSPOT_COMPARISON,
pipedrive: PIPEDRIVE_COMPARISON,
salesforce: SALESFORCE_COMPARISON,
sap: SAP_COMPARISON,
};
@@ -26,6 +26,7 @@ import {
type PlanTableCellType,
type PlanTableFeatureRowDataType,
type PlanTableTierColumnType,
type PlanTableTierId,
} from './plan-table-types';
import { resolveVisibleRows } from './plan-table-visible-rows';
@@ -41,7 +42,7 @@ const TableScope = styled.div`
const GridRow = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: 1.2fr 1fr 1fr 1fr;
width: 100%;
`;
@@ -134,6 +135,20 @@ const CtaRow = styled.div`
width: 100%;
`;
// Enterprise inherits the Organization cell unless a row overrides it: the
// tier is a superset, so unmarked rows read as "same as Organization".
function resolveCell(
row: PlanTableFeatureRowDataType,
columnId: PlanTableTierId,
): PlanTableCellType {
return (
row.tiers[columnId] ??
(columnId === 'enterprise' ? row.tiers.organization : undefined) ?? {
kind: 'dash',
}
);
}
function CellValue({ cell }: { cell: PlanTableCellType }) {
const { i18n } = useLingui();
@@ -167,7 +182,7 @@ function FeatureRow({
<FeatureLabel>{i18n._(row.featureLabel)}</FeatureLabel>
{tierColumns.map((column) => (
<TierCell key={column.id}>
<CellValue cell={row.tiers[column.id]} />
<CellValue cell={resolveCell(row, column.id)} />
</TierCell>
))}
</GridRow>
@@ -184,6 +199,7 @@ function CategoryRow({ title }: { title: MessageDescriptor }) {
</CategoryBand>
<CategoryBand aria-hidden="true" />
<CategoryBand aria-hidden="true" />
<CategoryBand aria-hidden="true" />
</GridRow>
);
}
@@ -234,7 +250,7 @@ export function PlanTableContent() {
<HeadCell>{i18n._(PLAN_TABLE_DATA.featureColumnLabel)}</HeadCell>
{PLAN_TABLE_DATA.tierColumns.map((column) => (
<HeadCell data-tier key={column.id}>
{i18n._(column.label)}
{i18n._(column.label[hosting])}
</HeadCell>
))}
</GridRow>
@@ -9,10 +9,12 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
{
featureLabel: msg`Price`,
selfHostTiers: {
enterprise: { kind: 'text', text: msg`From $50k/year` },
organization: { kind: 'text', text: msg`$19` },
pro: { kind: 'text', text: msg`$0` },
},
tiers: {
enterprise: { kind: 'text', text: msg`From $50k/year` },
organization: { kind: 'text', text: msg`$19` },
pro: { kind: 'text', text: msg`$9` },
},
@@ -269,6 +271,15 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
},
type: 'row',
},
{
featureLabel: msg`Dedicated support & SLA`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
featureLabel: msg`Onboarding Packs`,
selfHostTiers: {
@@ -353,6 +364,7 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
appliesTo: 'cloud',
featureLabel: msg`API calls`,
tiers: {
enterprise: { kind: 'text', text: msg`Custom` },
organization: { kind: 'text', text: msg`100 per minute` },
pro: { kind: 'text', text: msg`50 per minute` },
},
@@ -377,13 +389,79 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
},
type: 'row',
},
{ title: msg`Enterprise`, type: 'category' },
{
appliesTo: 'cloud',
featureLabel: msg`Single-tenant isolation`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
appliesTo: 'cloud',
featureLabel: msg`IP allow-listing`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
featureLabel: msg`SCIM provisioning`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
appliesTo: 'selfHost',
featureLabel: msg`Air-gapped deployment`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
appliesTo: 'selfHost',
featureLabel: msg`LTS releases`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
featureLabel: msg`Security review & DPA`,
tiers: {
enterprise: { kind: 'yes', label: msg`Yes` },
organization: { kind: 'dash' },
pro: { kind: 'dash' },
},
type: 'row',
},
],
seeMoreFeaturesCta: {
collapseLabel: msg`Show less`,
expandLabel: msg`See more features`,
},
tierColumns: [
{ id: 'pro', label: msg`Pro` },
{ id: 'organization', label: msg`Organization` },
{ id: 'pro', label: { cloud: msg`Pro`, selfHost: msg`Pro` } },
{
id: 'organization',
label: { cloud: msg`Organization`, selfHost: msg`Organization` },
},
{
id: 'enterprise',
label: { cloud: msg`Enterprise`, selfHost: msg`Enterprise` },
},
],
};
@@ -2,7 +2,7 @@ import { type MessageDescriptor } from '@lingui/core';
import { type PlansHostingMode } from '@/pricing-state';
export type PlanTableTierId = 'organization' | 'pro';
export type PlanTableTierId = 'enterprise' | 'organization' | 'pro';
export type PlanTableCellType =
| { kind: 'dash' }
@@ -11,7 +11,7 @@ export type PlanTableCellType =
export type PlanTableTierColumnType = {
id: PlanTableTierId;
label: MessageDescriptor;
label: Record<PlansHostingMode, MessageDescriptor>;
};
export type PlanTableCategoryRowDataType = {
@@ -23,8 +23,8 @@ export type PlanTableCategoryRowDataType = {
export type PlanTableFeatureRowDataType = {
appliesTo?: PlansHostingMode;
featureLabel: MessageDescriptor;
selfHostTiers?: Record<PlanTableTierId, PlanTableCellType>;
tiers: Record<PlanTableTierId, PlanTableCellType>;
selfHostTiers?: Partial<Record<PlanTableTierId, PlanTableCellType>>;
tiers: Partial<Record<PlanTableTierId, PlanTableCellType>>;
type: 'row';
};
@@ -36,7 +36,7 @@ const FIXTURE: PlanTableBodyRowDataType[] = [
];
const hasDashPro = (rows: PlanTableBodyRowDataType[]): boolean =>
rows.some((row) => row.type === 'row' && row.tiers.pro.kind === 'dash');
rows.some((row) => row.type === 'row' && row.tiers.pro?.kind === 'dash');
describe('resolveVisibleRows', () => {
it('drops self-host-only rows in cloud mode', () => {
@@ -1,11 +1,11 @@
'use client';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
import { TalkToUsButton } from '@/contact-cal';
import { useAnimatedNumber } from '@/platform/motion';
import { color, mediaUp, radius, semanticColor, spacing } from '@/tokens';
import { Body, Button, Heading } from '@/ui';
@@ -142,6 +142,7 @@ export function PlanCard({
const { i18n } = useLingui();
const tier = PLANS_DATA[tierId];
const cell = tier.cells[hosting][billing];
const cta = tier.cta[hosting];
const iconWidth = tier.icon.widthPx ?? 80;
const animatedPriceValue = useAnimatedNumber(cell.price.value);
const { comparisonBulletTexts, phase, visibleBullets } = useFeatureTransition(
@@ -159,11 +160,11 @@ export function PlanCard({
size="xs"
weight="light"
>
{i18n._(tier.heading)}
{i18n._(tier.heading[hosting])}
</Heading>
<PriceLine>
<Heading as="h4" family="sans" size="sm" weight="regular">
{`${cell.price.prefix}${PRICE_NUMBER_FORMATTER.format(animatedPriceValue)}`}
{`${cell.price.prefix}${PRICE_NUMBER_FORMATTER.format(animatedPriceValue)}${cell.price.valueSuffix ?? ''}`}
</Heading>
<Body as="span" className={priceSuffixClassName} size="sm">
{i18n._(cell.price.suffix)}
@@ -190,11 +191,18 @@ export function PlanCard({
/>
<CtaRow>
<Button
href="https://app.twenty.com/welcome"
label={i18n._(msg`Start for free`)}
variant={highlighted ? 'filled' : 'outlined'}
/>
{cta.href !== undefined ? (
<Button
href={cta.href}
label={i18n._(cta.label)}
variant={highlighted ? 'filled' : 'outlined'}
/>
) : (
<TalkToUsButton
label={cta.label}
variant={highlighted ? 'filled' : 'outlined'}
/>
)}
</CtaRow>
</CardShell>
);
@@ -68,7 +68,7 @@ const CardsGrid = styled.div`
${mediaUp('md')} {
column-gap: ${spacing(6)};
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(3, minmax(0, 1fr));
row-gap: 0;
}
`;
@@ -80,6 +80,7 @@ export function PricingBoard() {
const maxBullets = Math.max(
PLANS_DATA.pro.cells[hosting][billing].featureBullets.length,
PLANS_DATA.organization.cells[hosting][billing].featureBullets.length,
PLANS_DATA.enterprise.cells[hosting][billing].featureBullets.length,
);
return (
@@ -106,6 +107,12 @@ export function PricingBoard() {
maxBullets={maxBullets}
tierId="organization"
/>
<PlanCard
billing={billing}
hosting={hosting}
maxBullets={maxBullets}
tierId="enterprise"
/>
</CardsGrid>
</Board>
);
@@ -4,12 +4,13 @@ import { msg } from '@lingui/core/macro';
import { type PlansHostingMode } from '@/pricing-state';
export type PlansBillingPeriod = 'monthly' | 'yearly';
export type PlansTierId = 'organization' | 'pro';
export type PlansTierId = 'enterprise' | 'organization' | 'pro';
export type PlanPrice = {
prefix: string;
suffix: MessageDescriptor;
value: number;
valueSuffix?: string;
};
type PlansTierCell = {
@@ -17,9 +18,17 @@ type PlansTierCell = {
price: PlanPrice;
};
// A CTA without href opens the contact modal (TalkToUsButton) instead of
// navigating.
type PlansTierCta = {
href?: string;
label: MessageDescriptor;
};
type PlansTier = {
cells: Record<PlansHostingMode, Record<PlansBillingPeriod, PlansTierCell>>;
heading: MessageDescriptor;
cta: Record<PlansHostingMode, PlansTierCta>;
heading: Record<PlansHostingMode, MessageDescriptor>;
icon: { alt: string; src: string; widthPx?: number };
};
@@ -62,7 +71,63 @@ const ORGANIZATION_BULLETS_SELF_HOST = [
msg`Twenty team support`,
];
const ENTERPRISE_BULLETS_CLOUD = [
msg`Everything in Organization`,
msg`Single-tenant isolation`,
msg`IP allow-listing`,
msg`SCIM provisioning`,
msg`Dedicated support & SLA`,
];
const ENTERPRISE_BULLETS_SELF_HOST = [
msg`Everything in Organization`,
msg`SCIM provisioning`,
msg`Air-gapped deployment`,
msg`LTS releases`,
msg`Dedicated support & SLA`,
];
const ENTERPRISE_PRICE = {
value: 50,
valueSuffix: 'k',
prefix: 'from $',
suffix: msg`/year`,
};
export const PLANS_DATA: Record<PlansTierId, PlansTier> = {
enterprise: {
cells: {
cloud: {
monthly: {
featureBullets: ENTERPRISE_BULLETS_CLOUD,
price: ENTERPRISE_PRICE,
},
yearly: {
featureBullets: ENTERPRISE_BULLETS_CLOUD,
price: ENTERPRISE_PRICE,
},
},
selfHost: {
monthly: {
featureBullets: ENTERPRISE_BULLETS_SELF_HOST,
price: ENTERPRISE_PRICE,
},
yearly: {
featureBullets: ENTERPRISE_BULLETS_SELF_HOST,
price: ENTERPRISE_PRICE,
},
},
},
cta: {
cloud: { label: msg`Talk to sales` },
selfHost: { label: msg`Talk to sales` },
},
heading: { cloud: msg`Enterprise`, selfHost: msg`Enterprise` },
icon: {
alt: 'Enterprise plan icon',
src: '/images/pricing/plans/enterprise-icon.webp',
},
},
organization: {
cells: {
cloud: {
@@ -86,7 +151,17 @@ export const PLANS_DATA: Record<PlansTierId, PlansTier> = {
},
},
},
heading: msg`Organization`,
cta: {
cloud: {
href: 'https://app.twenty.com/welcome',
label: msg`Start for free`,
},
selfHost: {
href: 'https://app.twenty.com/welcome',
label: msg`Start for free`,
},
},
heading: { cloud: msg`Organization`, selfHost: msg`Organization` },
icon: {
alt: 'Organization plan icon',
src: '/images/pricing/plans/organization-icon.webp',
@@ -115,7 +190,17 @@ export const PLANS_DATA: Record<PlansTierId, PlansTier> = {
},
},
},
heading: msg`Pro`,
cta: {
cloud: {
href: 'https://app.twenty.com/welcome',
label: msg`Start for free`,
},
selfHost: {
href: 'https://app.twenty.com/welcome',
label: msg`Start for free`,
},
},
heading: { cloud: msg`Pro`, selfHost: msg`Pro` },
icon: {
alt: 'Pro plan icon',
src: '/images/pricing/plans/pro-icon.webp',
@@ -2,12 +2,36 @@ import { styled } from '@linaria/react';
import { msg } from '@lingui/core/macro';
import { getServerI18n } from '@/platform/i18n/get-server-i18n';
import { mediaUp, spacing } from '@/tokens';
import { LocalizedLink } from '@/platform/i18n/LocalizedLink';
import {
color,
DURATION,
fontSize,
mediaUp,
semanticColor,
spacing,
} from '@/tokens';
import { Body, Heading, SectionShell } from '@/ui';
import { SALESFARCE_DATA } from './salesfarce-data';
import { SalesfarceFlow } from './SalesfarceFlow';
const COMPARE_LINKS = [
{ label: msg`Twenty vs HubSpot`, href: '/compare-pricing/hubspot' },
{ label: msg`Twenty vs Salesforce`, href: '/compare-pricing/salesforce' },
{
label: msg`Twenty vs SAP Sales Cloud`,
href: '/compare-pricing/sap-sales-cloud',
},
{ label: msg`Twenty vs Pipedrive`, href: '/compare-pricing/pipedrive' },
{
label: msg`Twenty vs Microsoft Dynamics`,
href: '/compare-pricing/microsoft-dynamics',
},
// Attio comparison parked, may return:
// { label: msg`Twenty vs Attio`, href: '/compare-pricing/attio' },
];
const Grid = styled.div`
display: grid;
grid-template-columns: 1fr;
@@ -43,6 +67,57 @@ const CopyColumn = styled.div`
}
`;
const CompareList = styled.ul`
display: flex;
flex-direction: column;
gap: ${spacing(2)};
list-style: none;
margin-top: ${spacing(6)};
padding: 0;
`;
// The footer's hover-marker link affordance, so the list reads as the same
// navigation language.
const HoverMarker = styled.span`
background-color: ${semanticColor.ink};
border-radius: 1px;
display: inline-flex;
flex-shrink: 0;
height: 7px;
opacity: 0;
transition:
width ${DURATION.md} ease-out,
opacity ${DURATION.md} ease-out;
width: 0;
`;
const CompareLink = styled(LocalizedLink)`
align-items: center;
color: ${semanticColor.ink};
display: flex;
font-size: ${fontSize(4)};
gap: 0;
line-height: 1.35;
text-decoration: none;
transition: gap ${DURATION.md} ease-out;
&:focus-visible {
outline: 1px solid ${color('blue')};
outline-offset: 1px;
}
${mediaUp('md')} {
&:hover {
gap: ${spacing(2)};
}
&:hover [data-slot='hover-marker'] {
opacity: 1;
width: 14px;
}
}
`;
export function PricingSalesfarce() {
const i18n = getServerI18n();
@@ -51,11 +126,21 @@ export function PricingSalesfarce() {
<Grid>
<CopyColumn>
<Heading as="h2" size="lg" weight="light">
{i18n._(msg`Trust the n°1 CRM, *or not!*`)}
{i18n._(msg`Compare *the real cost*`)}
</Heading>
<Body muted size="sm">
{i18n._(SALESFARCE_DATA.body)}
</Body>
<CompareList>
{COMPARE_LINKS.map((link) => (
<li key={link.href}>
<CompareLink href={link.href}>
<HoverMarker aria-hidden data-slot="hover-marker" />
{i18n._(link.label)}
</CompareLink>
</li>
))}
</CompareList>
</CopyColumn>
<SalesfarceFlow pricing={SALESFARCE_DATA.pricing} />
</Grid>
@@ -5,7 +5,7 @@ import { type SalesfarceDataType } from './salesfarce-types';
const SALESFARCE_POPUP_TITLE = msg`Good choice!`;
export const SALESFARCE_DATA: SalesfarceDataType = {
body: msg`Some call this enterprise pricing. We prefer a CRM where API access, webhooks, and workflows don't show up as surprise add-ons.`,
body: msg`Feature-by-feature price checks against the CRMs you're probably comparing us with. Sources included.`,
pricing: {
addons: [
{