diff --git a/src/pages/MarketingPage.test.tsx b/src/pages/MarketingPage.test.tsx
index 1554616..454534e 100644
--- a/src/pages/MarketingPage.test.tsx
+++ b/src/pages/MarketingPage.test.tsx
@@ -121,6 +121,46 @@ describe('MarketingPage — claim consistency (T18 P1-4 / P1-6)', () => {
expect(text).toMatch(/~60s/)
})
+ // TEAM-GATE reconciliation (2026-06-08): the homepage Team tile's CTA used
+ // to point at the self-serve /app/checkout?plan=team path — a tier the
+ // server-side gate rejects with 400 tier_not_yet_available, so the homepage
+ // was marketing a plan the platform can't actually sell. Team is
+ // contact-sales only until its delivery is proven built (CEO TEAM-GATE
+ // directive). These guards pin the homepage Team CTA to a contact-sales
+ // mailto and forbid the self-serve checkout link from creeping back, AND
+ // confirm Team is NOT badged "Most popular" (Pro is the highlighted tier).
+ it('homepage Team tile CTA is a contact-sales mailto, NOT a self-serve checkout', () => {
+ const { container } = render(
+
+
+ ,
+ )
+ const teamCta = findAnchorByText('Contact sales →')
+ expect(teamCta).not.toBeNull()
+ expect(teamCta!.getAttribute('href')).toContain('mailto:sales@instanode.dev')
+ // The retired self-serve Team checkout link must not exist anywhere on the page.
+ const hrefs = Array.from(container.querySelectorAll('a')).map((a) => a.getAttribute('href') ?? '')
+ expect(hrefs.some((h) => h.includes('plan=team'))).toBe(false)
+ // And the old buyable label must be gone.
+ expect(findAnchorByText('Start team →')).toBeNull()
+ })
+
+ it('only Pro is badged "Most popular" — Team must not be highlighted/badged as buyable', () => {
+ const { container } = render(
+
+
+ ,
+ )
+ const badges = Array.from(container.querySelectorAll('.mkt-featured-flag')).filter(
+ (el) => (el.textContent ?? '').trim() === 'Most popular',
+ )
+ expect(badges.length).toBe(1)
+ // The badge sits inside the Pro card, not the Team card.
+ const card = badges[0].closest('.mkt-price-card')
+ expect(card?.textContent).toContain('Pro')
+ expect(card?.textContent).not.toContain('Team')
+ })
+
// BIZ-3 (2026-05-29): the landing pricing tile shipped "1 small deployment"
// and "10 medium deployments" copy from the days when /deploy/new had a
// deployment_size field. The backend dropped that field; marketing
diff --git a/src/pages/MarketingPage.tsx b/src/pages/MarketingPage.tsx
index 9d3f5e6..365bbf5 100644
--- a/src/pages/MarketingPage.tsx
+++ b/src/pages/MarketingPage.tsx
@@ -94,6 +94,13 @@ const ROUTES = {
playground: '#playground',
} as const
+// Team is sales-assisted only (TEAM-GATE, 2026-06-04 CEO directive): it is NOT
+// self-serve and must not route to /app/checkout until its dedicated-infra
+// delivery is proven built — the server-side gate rejects a Team checkout with
+// 400 tier_not_yet_available. The Team CTA points here, matching the
+// contact-sales action PricingPage.tsx and BillingPage.tsx already use.
+const SALES_MAILTO_TEAM = 'mailto:sales@instanode.dev?subject=Team%20plan%20enquiry'
+
type Service = {
id: 'pg' | 'rd' | 'mg' | 'vc' | 'qu' | 'st' | 'wh' | 'dp'
name: string
@@ -203,12 +210,13 @@ const PLANS: Plan[] = [
// Team tier — $199/mo. strict-80% margin redesign (2026-06-05): every
// Team limit is now a finite plans.yaml cap (was -1/unlimited). Above
// these caps = Enterprise (contact sales).
- // NOTE (pre-existing, out of scope here): this CTA still points at the
- // self-serve /app/checkout?plan=team path, which the server-side Team
- // gate rejects with 400 tier_not_yet_available. The PricingPage Team CTA
- // already uses a contact-sales mailto. This MarketingPage CTA should be
- // reconciled with the gate in a follow-up (not changed here — the
- // strict-margin task is explicitly copy-only and must not touch the gate).
+ // TEAM-GATE reconciliation (2026-06-08): the CTA previously pointed at the
+ // self-serve /app/checkout?plan=team path, which the server-side Team gate
+ // rejects with 400 tier_not_yet_available — so the homepage was offering a
+ // tier the platform can't sell. Now reconciled with PricingPage.tsx +
+ // BillingPage.tsx: Team is contact-sales only until its delivery is proven
+ // built. Do NOT re-point this at /app/checkout. Ref TEAM-GATE directive
+ // (2026-06-04) + docs/sessions/2026-06-04/TEAM-PLAN-GATE-AND-BUILD.md.
id: 'team',
name: 'Team',
tagline: 'For the engineering org. Dedicated infra, RBAC + audit, with SSO & SLA on the way.',
@@ -221,8 +229,8 @@ const PLANS: Plan[] = [
'Need more? Enterprise — contact sales',
],
cta: {
- label: 'Start team →',
- href: '/app/checkout?plan=team&frequency=monthly',
+ label: 'Contact sales →',
+ href: SALES_MAILTO_TEAM,
variant: 'secondary',
},
},
@@ -311,7 +319,7 @@ export function MarketingPage() {
Postgres, Redis, MongoDB, vectors, queues, storage, webhooks, and deployments.{' '}
Provisioned in <2 seconds.{' '}
- No signup, no Docker, no waitlist.
+ Self-serve from the first call to a paid plan — no signup, no Docker, no sales call.