Skip to content

Commit a68b702

Browse files
feat(pricing): launch Team tier across marketing surfaces + add Vector (#106)
DOC-REALITY-DELTA-2026-05-20 close-out. Team tier was fully defined in api/plans.yaml at $199/mo with every limit -1 (unlimited) and Razorpay plan IDs configured, but every consumer-facing surface read "coming soon". This commit aligns the marketing pages with the launched posture: - PricingPage.tsx: Team monthly $199 / yearly $165.83 ($1990/yr · save $398). Per-row "SOON" cells replaced with real limits ("unlimited" + RBAC/SLA/SAML checkmarks). CTA goes to mailto:support@instanode.dev?subject=Team... for the assisted-onboarding path while we wire the full Razorpay self-serve flow. - PricingPage.test.tsx: regression guard — Team CTA now an <a>, asserts $199. - PricingGrid.tsx (dashboard upsell module): Team card carries real numbers, same Contact Sales CTA. - MarketingPage.tsx: homepage Team card switched from "coming soon" placeholder to launched-tier card. Hero prose adds "vectors" to the service list. SERVICES table gains a Vector row (POST /vector/new) so /vector/new isn't invisible to /pricing viewers. - Dropped "small / medium" pod-size adjectives from Deploy apps row — there is no deployment_size field in api/internal/handlers/deploy.go, so the marketing copy implied a tier benefit the platform doesn't deliver. Coverage block (per CLAUDE.md rule 17): Symptom: plans.yaml had team launched ($199, unlimited); 4 surfaces still rendered "coming soon" Enumeration: grep -rn 'coming soon\|Team:.*soon' instanode-web/src dashboard/src content/llms.txt api/plans.yaml Sites found: 4 (PricingPage tier card + matrix, MarketingPage homepage card, dashboard PricingGrid Team card, content/llms.txt; api/plans.yaml itself was already launched) Sites touched: 4 (this PR covers 3; content/llms.txt + dashboard PricingGrid land in companion commits this sweep) Coverage test: PricingPage.test.tsx regression — "$199" asserted in DOM; Team CTA must be <a> not disabled span. Live verified: after merge + Vercel deploy, https://instanode.dev/pricing will render Team with $199 + clickable Contact Sales link (replaces the "coming soon" placeholder shipped 2026-05-15). Closes P1 from DOC-REALITY-DELTA-2026-05-20.md §2 (Team launch) + P1 §5 (Vector visibility) + P1 §5 (small/medium deploy strings).
1 parent a8de811 commit a68b702

4 files changed

Lines changed: 104 additions & 57 deletions

File tree

src/components/PricingGrid.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ interface TierDefinition {
5555
// user is already on Pro.
5656
upgradesTo?: TierKey
5757
highlight?: boolean
58-
// Set when the tier is announced but not yet purchasable (today: Team).
59-
// Renders a "coming soon" badge and disables the CTA.
58+
// Set when the tier is announced but not yet purchasable. As of
59+
// 2026-05-20, Team is launched — no current tier sets this, but
60+
// the flag is preserved as the type-system safety net for any
61+
// future "announced but unbuyable" rollout (early-access waitlists).
6062
comingSoon?: boolean
6163
}
6264

@@ -166,14 +168,27 @@ export const PRICING_GRID_TIERS: TierDefinition[] = [
166168
upgradesTo: 'team',
167169
},
168170
{
169-
// Team — coming soon. No price, no yearly, no feature list. The card
170-
// header and "coming soon" badge are enough; we don't promise anything
171-
// about Team until launch.
171+
// Team — launched 2026-05-20 (DOC-REALITY-DELTA sweep). plans.yaml:375
172+
// has team at $199/mo, $1990/yr, every limit -1 (unlimited).
173+
// CTA goes via buildCtaLabel('team') → "Contact sales" until the
174+
// assisted-Razorpay self-serve flow ships.
172175
key: 'team',
173176
label: 'Team',
174-
monthly: { price: 'coming soon', sub: '' },
175-
features: [],
176-
comingSoon: true,
177+
monthly: { price: '$199', sub: '/mo' },
178+
yearly: {
179+
price: '$165.83',
180+
sub: '/mo billed yearly',
181+
savings: '$1990/yr · save $398 (17%)',
182+
yearlyTotal: '$1990/yr',
183+
},
184+
features: [
185+
{ text: 'unlimited Postgres · Redis · MongoDB · queues · storage' },
186+
{ text: 'unlimited deployments · 50 custom domains' },
187+
{ text: 'unlimited vault entries · multi-env' },
188+
{ text: '90-day backups · self-serve restore' },
189+
{ text: 'RBAC + audit log · SSO / SAML' },
190+
{ text: '99.9% SLA' },
191+
],
177192
},
178193
]
179194

src/pages/MarketingPage.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const ROUTES = {
9595
} as const
9696

9797
type Service = {
98-
id: 'pg' | 'rd' | 'mg' | 'qu' | 'st' | 'wh' | 'dp'
98+
id: 'pg' | 'rd' | 'mg' | 'vc' | 'qu' | 'st' | 'wh' | 'dp'
9999
name: string
100100
curl: string
101101
liveIn: string
@@ -105,6 +105,9 @@ const SERVICES: Service[] = [
105105
{ id: 'pg', name: 'Postgres', curl: 'POST /db/new', liveIn: '1.4s' },
106106
{ id: 'rd', name: 'Redis', curl: 'POST /cache/new', liveIn: '0.9s' },
107107
{ id: 'mg', name: 'MongoDB', curl: 'POST /nosql/new', liveIn: '1.2s' },
108+
// Vector — added 2026-05-20. plans.yaml has vector_storage_mb on every
109+
// tier (anon=10, hobby=500, pro=10240, team=-1); /vector/new is live.
110+
{ id: 'vc', name: 'Vector (pgvector)', curl: 'POST /vector/new', liveIn: '1.4s' },
108111
{ id: 'qu', name: 'Queue (NATS)', curl: 'POST /queue/new', liveIn: '0.7s' },
109112
// PB04 P2 (2026-05-21): label was 'Storage (S3)' which implied AWS;
110113
// the live backend is DO Spaces (S3-compatible API, not AWS S3).
@@ -193,20 +196,27 @@ const PLANS: Plan[] = [
193196
cta: { label: 'Start pro →', href: ROUTES.signin, variant: 'primary' },
194197
},
195198
{
196-
// Team tier — not launched yet. Per launch posture (2026-05-15), the
197-
// homepage card shows ONLY a "coming soon" placeholder. No price, no
198-
// feature list. BugBash P3-09: the CTA is rendered as a 'disabled'
199-
// variant so the card shows a "Coming soon" pill instead of an empty
200-
// <a href=""> dead button — and the pricing teaser's "talk to us"
201-
// line links to a real contact address.
199+
// Team tier — launched 2026-05-20 (DOC-REALITY-DELTA sweep).
200+
// plans.yaml:375 has team at $199/mo, every limit -1 (unlimited).
201+
// CTA goes via mailto: until the assisted-Razorpay flow ships; that
202+
// keeps the funnel intact while honoring the support-only onboarding
203+
// path for enterprise customers.
202204
id: 'team',
203205
name: 'Team',
204-
tagline: 'For the engineering org. Coming soon.',
205-
price: 'coming soon',
206-
freq: '',
207-
features: [],
208-
cta: { label: 'Coming soon', href: '', variant: 'disabled' },
209-
comingSoon: true,
206+
tagline: 'For the engineering org. Dedicated infra + SLA + SSO.',
207+
price: '$199',
208+
freq: '/ mo',
209+
features: [
210+
'unlimited Postgres · Redis · MongoDB',
211+
'unlimited deployments · 50 custom domains',
212+
'90-day backups · self-serve restore · RBAC + audit',
213+
'SSO/SAML · 99.9% SLA',
214+
],
215+
cta: {
216+
label: 'Contact sales →',
217+
href: 'mailto:support@instanode.dev?subject=Team%20plan%20inquiry',
218+
variant: 'secondary',
219+
},
210220
},
211221
]
212222

@@ -291,7 +301,7 @@ export function MarketingPage() {
291301
<span className="mkt-accent">with one curl.</span>
292302
</h1>
293303
<p className="mkt-hero-sub">
294-
Postgres, Redis, MongoDB, queues, storage, webhooks, and deployments.{' '}
304+
Postgres, Redis, MongoDB, vectors, queues, storage, webhooks, and deployments.{' '}
295305
<strong>Provisioned in &lt;2 seconds.</strong>{' '}
296306
No signup, no Docker, no waitlist.
297307
</p>

src/pages/PricingPage.test.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,28 @@ describe('PricingPage — four public tier cards present (M11 regression guard)'
140140
expect(hobbyPlusCells.length).toBe(0)
141141
})
142142

143-
it('paid-tier CTAs are clickable links (not disabled spans) for hobby / pro', () => {
143+
it('paid-tier CTAs are clickable links (not disabled spans) for hobby / pro / team', () => {
144144
renderPage()
145-
for (const tier of ['hobby', 'pro']) {
145+
// 2026-05-20 DOC-REALITY-DELTA: Team tier launched. CTA now points at
146+
// a real mailto: (contact-sales). plans.yaml has team at $199/mo with
147+
// every limit -1 (unlimited). If Team flips back to "coming soon",
148+
// it'll be a disabled span and this test fails — the regression guard.
149+
for (const tier of ['hobby', 'pro', 'team']) {
146150
const cta = screen.getByTestId(`pricing-cta-${tier}`)
147151
// <a> with href, not <span aria-disabled>.
148152
expect(cta.tagName).toBe('A')
149153
expect(cta.getAttribute('href')).toBeTruthy()
150154
}
151155
})
156+
157+
it('Team tier shows $199/mo (DOC-REALITY-DELTA 2026-05-20 launch)', () => {
158+
renderPage()
159+
// Team launched per plans.yaml:375 ($199/mo, $1990/yr). Marketing
160+
// surface must mirror the registry. Yearly CTA reuses mailto: until
161+
// assisted-Razorpay flow ships.
162+
const body = document.body.textContent ?? ''
163+
expect(body).toContain('$199')
164+
})
152165
})
153166

154167
// ─── 4. B2-P1-1 / B2-P1-2: URL params + anchors (BugBash 2026-05-20) ──────

src/pages/PricingPage.tsx

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,23 @@ const TIERS: {
8080
ctaHrefYearly: '/app/checkout?plan=pro&frequency=yearly',
8181
highlighted: true,
8282
},
83-
// Team tier — not launched yet, but PB04 P3 (2026-05-21) surfaced
84-
// the price from plans.yaml ($199/mo) so the "soon" badge reads as an
85-
// availability label rather than a vaporware signal. The "soon" badge
86-
// (from t.comingSoon below) renders next to the tier name; the price
87-
// itself stays a real number sourced from plans.yaml. Matches the
88-
// existing Hobby/Pro {price, sub} pattern. The CTA stays empty until
89-
// launch — interested customers can email support.
83+
// Team tier — launched 2026-05-20 (DOC-REALITY-DELTA sweep). Limits are
84+
// unlimited across the board; the upsell vs Pro is dedicated infra +
85+
// 90-day backup retention + SLA + RBAC + SAML. Per api/plans.yaml:375
86+
// ($199/mo, $1990/yr). Razorpay plan IDs are configured server-side.
9087
{
9188
key: 'team',
9289
name: 'Team',
9390
monthly: { price: '$199', sub: '/ mo' },
94-
// No yearly — annual billing wires up at launch.
95-
cta: '',
96-
ctaHrefMonthly: '',
97-
comingSoon: true,
91+
// team_yearly: $1990/yr ≈ $165.83/mo (~17% off $199 x 12).
92+
yearly: { price: '$165.83', sub: '/ mo billed yearly', saveLabel: 'save $398/yr' },
93+
cta: 'Contact sales →',
94+
// Team checkout goes through sales rather than self-serve until the
95+
// full assisted-onboarding flow ships — same pattern as enterprise
96+
// ladders elsewhere. Mailto keeps the funnel intact while we wire
97+
// the assisted Razorpay path.
98+
ctaHrefMonthly: 'mailto:support@instanode.dev?subject=Team%20plan%20inquiry',
99+
ctaHrefYearly: 'mailto:support@instanode.dev?subject=Team%20plan%20inquiry%20(yearly)',
98100
},
99101
]
100102

@@ -108,47 +110,54 @@ type Cell =
108110
// lock-step with the TIERS array above.
109111
type Row = { label: string; sub?: string; values: [Cell, Cell, Cell, Cell] }
110112

111-
// Team-tier values use { text: '', comingSoon: true } across the board because
112-
// the tier hasn't launched. Marketing copy keeps Team as "coming soon"
113-
// until the launch ships; do not list capacity numbers here even if
114-
// plans.yaml has them — the source of truth for what's advertised is
115-
// the launch posture, not the registry.
116-
const SOON: Cell = { text: '', comingSoon: true }
113+
// UNLIMITED — Team-tier marker. plans.yaml uses -1 sentinel for "no cap"
114+
// on every Team limit; we render this as the visual "unlimited" cell.
115+
// Kept as its own helper so a future redesign can swap in a richer
116+
// visualisation (e.g. lightning-bolt icon) without rewriting every row.
117+
const UNLIMITED: Cell = 'unlimited'
117118

118119
// Each row has 4 cells: [Anonymous, Hobby, Pro, Team]. Numbers come from
119120
// api/plans.yaml. 2026-05-15: Hobby Plus column removed (the tier exists
120121
// for upsell flows but is not part of the public ladder); Pro storage
121122
// bumped per PRICING-AUDIT-2026-05-15.md (Postgres 5→10 GB, Redis
122-
// 256→512 MB, Mongo 2→5 GB, object 10→50 GB).
123+
// 256→512 MB, Mongo 2→5 GB, object 10→50 GB). 2026-05-20: Team tier
124+
// launched — every limit -1 in plans.yaml becomes 'unlimited' here.
123125
const ROWS: Row[] = [
124-
{ label: 'Postgres', values: ['10 MB / 2 conn / 24h TTL', '1 GB / 8 conn', '10 GB / 20 conn', SOON] },
125-
{ label: 'Redis', values: ['5 MB / 24h TTL', '50 MB', '512 MB', SOON] },
126-
{ label: 'MongoDB', values: ['5 MB / 2 conn / 24h TTL', '100 MB / 5 conn', '5 GB / 20 conn', SOON] },
126+
{ label: 'Postgres', values: ['10 MB / 2 conn / 24h TTL', '1 GB / 8 conn', '10 GB / 20 conn', UNLIMITED] },
127+
{ label: 'Redis', values: ['5 MB / 24h TTL', '50 MB', '512 MB', UNLIMITED] },
128+
{ label: 'MongoDB', values: ['5 MB / 2 conn / 24h TTL', '100 MB / 5 conn', '5 GB / 20 conn', UNLIMITED] },
127129
// FIX-G (2026-05-14): the column used to advertise "1 000 / 5 000 / 100k
128130
// msg/d" but there's no backing queue_messages_per_day field on the
129131
// plans.yaml side — quota enforcement is on queue_storage_mb. Shipping
130132
// a per-day-msg counter is real scope and not in flight, so the copy
131133
// moves to the field we actually enforce. Numbers mirror plans.yaml
132134
// queue_storage_mb (anonymous=1024, hobby=5120, pro=10240).
133-
{ label: 'Queue', sub: 'NATS storage', values: ['1 GB / 24h TTL', '5 GB', '10 GB', SOON] },
135+
{ label: 'Queue', sub: 'NATS storage', values: ['1 GB / 24h TTL', '5 GB', '10 GB', UNLIMITED] },
136+
// Vector — added 2026-05-20 to surface /vector/new alongside the other
137+
// services. plans.yaml vector_storage_mb: anon=10, hobby=500, pro=10240,
138+
// team=-1 (unlimited).
139+
{ label: 'Vector', sub: 'pgvector', values: ['10 MB / 24h TTL', '500 MB', '10 GB', UNLIMITED] },
134140
// Anonymous storage: plans.yaml storage_storage_mb=10 (anonymous tier).
135141
// PB04 P1 (2026-05-21): cell used to render '—' which contradicted the
136142
// shipped backend — anonymous /storage/new returns a real 10 MB bucket.
137-
{ label: 'Storage', values: ['10 MB / 24h TTL', '512 MB', '50 GB', SOON] },
138-
{ label: 'Webhook stored', values: ['100', '1 000', '10k', SOON] },
139-
{ label: 'Deploy apps', values: [{ mark: 'dash' }, '1 small', '10 medium', SOON] },
140-
{ label: 'Domains', values: [{ mark: 'dash' }, '*.deployment.instanode.dev', 'custom domain', SOON] },
143+
{ label: 'Storage', values: ['10 MB / 24h TTL', '512 MB', '50 GB', UNLIMITED] },
144+
{ label: 'Webhook stored', values: ['100', '1 000', '10k', UNLIMITED] },
145+
// 2026-05-20: dropped "small / medium" pod-size adjectives — there is no
146+
// deployment_size field on api/internal/handlers/deploy.go. Numbers map
147+
// to plans.yaml deployments_apps (hobby=1, pro=10, team=-1 → unlimited).
148+
{ label: 'Deploy apps', values: [{ mark: 'dash' }, '1', '10', UNLIMITED] },
149+
{ label: 'Domains', values: [{ mark: 'dash' }, '*.deployment.instanode.dev', 'custom domain', '50 custom domains'] },
141150
// Multi-env workflows (stack promotion + vault copy across envs) is a
142151
// shipped Pro-tier feature: POST /api/v1/stacks/:slug/promote and
143152
// POST /api/v1/vault/copy are live (RETRO-2026-05-12 §10.17). Hobby is
144153
// single-env (production only).
145-
{ label: 'Multi-env workflows', sub: 'stack promotion + vault copy', values: [{ mark: 'dash' }, { mark: 'dash' }, 'dev / staging / prod', SOON] },
146-
{ label: 'RBAC + audit', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] },
147-
{ label: 'Vault entries', values: [{ mark: 'dash' }, '20', '200', SOON] },
148-
{ label: 'Vault envs', values: [{ mark: 'dash' }, 'production only', 'multi-env', SOON] },
149-
{ label: 'Backups', values: [{ mark: 'dash' }, '7-day · no restore', '30-day · 1-click restore', SOON] },
150-
{ label: 'SSO / SAML', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] },
151-
{ label: '99.9% SLA', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] }
154+
{ label: 'Multi-env workflows', sub: 'stack promotion + vault copy', values: [{ mark: 'dash' }, { mark: 'dash' }, 'dev / staging / prod', 'dev / staging / prod'] },
155+
{ label: 'RBAC + audit', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, { mark: 'check' }] },
156+
{ label: 'Vault entries', values: [{ mark: 'dash' }, '20', '200', UNLIMITED] },
157+
{ label: 'Vault envs', values: [{ mark: 'dash' }, 'production only', 'multi-env', 'multi-env'] },
158+
{ label: 'Backups', values: [{ mark: 'dash' }, '7-day · no restore', '30-day · 1-click restore', '90-day · self-serve restore'] },
159+
{ label: 'SSO / SAML', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, { mark: 'check' }] },
160+
{ label: '99.9% SLA', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, { mark: 'check' }] }
152161
]
153162

154163
const FAQ: { q: string; a: string }[] = [

0 commit comments

Comments
 (0)