diff --git a/src/components/PricingGrid.tsx b/src/components/PricingGrid.tsx index 8c8d5a0..6302cde 100644 --- a/src/components/PricingGrid.tsx +++ b/src/components/PricingGrid.tsx @@ -55,6 +55,9 @@ interface TierDefinition { // user is already on Pro. upgradesTo?: TierKey highlight?: boolean + // Set when the tier is announced but not yet purchasable (today: Team). + // Renders a "coming soon" badge and disables the CTA. + comingSoon?: boolean } // Numbers are USD and come from api/plans.yaml. "2 months free" framing @@ -150,31 +153,25 @@ export const PRICING_GRID_TIERS: TierDefinition[] = [ yearlyTotal: '$490/yr', }, features: [ - { text: '5 GB Postgres · 20 conn' }, - { text: '256 MB Redis' }, - { text: '2 GB MongoDB · 20 conn' }, - { text: '10 medium deployments' }, + // 2026-05-15 Pro storage bump — keep in sync with api/plans.yaml. + { text: '10 GB Postgres · 20 conn' }, + { text: '512 MB Redis' }, + { text: '5 GB MongoDB · 20 conn' }, + { text: '50 GB object storage · 10 medium deployments' }, { text: '200 vault entries · multi-env' }, { text: 'custom domain · 10k stored webhooks' }, ], upgradesTo: 'team', }, { + // Team — coming soon. No price, no yearly, no feature list. The card + // header and "coming soon" badge are enough; we don't promise anything + // about Team until launch. key: 'team', label: 'Team', - monthly: { price: '$199', sub: '/mo' }, - yearly: { - price: '$165.83', - sub: '/mo, billed yearly', - savings: '$1990/yr · save $398', - yearlyTotal: '$1990/yr', - }, - features: [ - { text: 'Everything in Pro, with larger limits', comingSoon: true }, - { text: 'Multi-seat · RBAC + audit log', comingSoon: true }, - { text: 'SSO / SAML · 99.9% SLA', comingSoon: true }, - { text: 'Dedicated node pools', comingSoon: true }, - ], + monthly: { price: 'coming soon', sub: '' }, + features: [], + comingSoon: true, }, ] diff --git a/src/components/upgradeCopy.ts b/src/components/upgradeCopy.ts index 9743410..f872308 100644 --- a/src/components/upgradeCopy.ts +++ b/src/components/upgradeCopy.ts @@ -130,8 +130,8 @@ export const UPGRADE_COPY: Record = { secondaryCtaFrequency: 'monthly', }, quota_wall: { - title: "You're approaching your hobby quota — Pro Annual unlocks 10x", - body: 'Upgrade to Pro for 5 GB Postgres (10x) and 256 MB Redis. Your existing resources keep working — limits raise immediately.', + title: "You're approaching your hobby quota — Pro unlocks 10× everything", + body: 'Upgrade to Pro for 10 GB Postgres, 512 MB Redis, 50 GB object storage, 10 deploys. Your existing resources keep working — limits raise immediately.', priceLine: '$40.83/mo billed yearly · 2 months free · resources elevate instantly', primaryCtaLabel: PRIMARY_CTA_LABEL_PRO_ANNUAL, primaryCtaFrequency: 'yearly', diff --git a/src/pages/ForAgentsPage.tsx b/src/pages/ForAgentsPage.tsx index d26bf8b..2f13ad9 100644 --- a/src/pages/ForAgentsPage.tsx +++ b/src/pages/ForAgentsPage.tsx @@ -31,6 +31,11 @@ const REASONS: { eyebrow: string; body: string }[] = [ eyebrow: '03 · idempotent claim', body: "Idempotent claim. The same JWT can be claimed exactly once — atomically. Your agent's logic is deterministic." + }, + { + eyebrow: '04 · safe retries on every create', + body: + 'Every create endpoint deduplicates retries. Pass an Idempotency-Key header for true exactly-once across a 24h window, or just retry safely — the server fingerprints (scope + route + canonical body) and replays for 120s. The response header X-Idempotent-Replay: true tells you when you hit the cache.' } ] @@ -324,10 +329,13 @@ function ForAgentsStyles() { /* reasons */ .fa-reasons { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 14px; } - @media (max-width: 980px) { + @media (max-width: 1180px) { + .fa-reasons { grid-template-columns: repeat(2, 1fr); } + } + @media (max-width: 640px) { .fa-reasons { grid-template-columns: 1fr; } } .fa-reason { diff --git a/src/pages/MarketingPage.tsx b/src/pages/MarketingPage.tsx index 867c2c9..8de07c6 100644 --- a/src/pages/MarketingPage.tsx +++ b/src/pages/MarketingPage.tsx @@ -107,21 +107,13 @@ const PLANS: Plan[] = [ ], cta: { label: 'Start hobby →', href: ROUTES.signin, variant: 'secondary' }, }, - { - id: 'hobby_plus', - name: 'Hobby Plus', - tagline: 'For the side project that wants a vanity URL and a backup safety net.', - price: '$19', - freq: '/ mo', - features: [ - '1 GB Postgres · 8 conn', - '50 MB Redis · 1 GB Mongo · 5 conn', - '2 deployments · custom domain included', - '50 vault entries · multi-env (dev / staging / prod)', - '14-day backups · 1-click restore', - ], - cta: { label: 'Start hobby plus →', href: ROUTES.signin, variant: 'secondary' }, - }, + // 2026-05-15: Hobby Plus removed from the marketing page to avoid + // cluttering the public tier ladder (Anonymous / Hobby / Pro / Team + // tells the cleaner story for first-time visitors). The tier remains + // a real, paid plan in api/plans.yaml and is reachable via dashboard + // upsell paths (quota_wall, custom_domain prompts, in-product nudges). + // Re-add this card if Hobby Plus becomes an outbound funnel rather + // than an upsell-only step. { id: 'pro', name: 'Pro', @@ -130,31 +122,26 @@ const PLANS: Plan[] = [ freq: '/ mo', featured: true, features: [ - '5 GB Postgres · 20 conn', - '256 MB Redis · 2 GB Mongo · 20 conn', - '10 medium deployments · custom domain', + '10 GB Postgres · 20 conn', + '512 MB Redis · 5 GB Mongo · 20 conn', + '50 GB object storage · 10 medium deployments · custom domain', '200 vault entries · multi-env (dev/staging/prod + custom)', ], cta: { label: 'Start pro →', href: ROUTES.signin, variant: 'primary' }, }, { + // Team tier — not launched yet. Per launch posture (2026-05-15), the + // homepage card shows ONLY a "coming soon" placeholder. No price, no + // feature list, no CTA. The full Team launch will swap this card to + // a real tier once multi-seat / RBAC / SSO are shipped. id: 'team', name: 'Team', - tagline: 'For the engineering org with envs, vault, and an audit trail.', - price: '$199', - freq: '/ mo', - features: [ - 'Everything in Pro, with larger per-resource limits', - 'Multi-seat workspace · RBAC + audit log (rolling out)', - 'SSO / SAML · 99.9% SLA (on roadmap)', - 'Dedicated node pools · priority support', - ], - // Multi-seat / RBAC / SSO UI is still being built (flagged inline in - // the `features` list as "rolling out" / "on roadmap"). The Razorpay - // yearly plan and dedicated-infra k8s plumbing both exist, so Team - // is sellable via enterprise@ today. Replacing the dead `#` anchor - // with a real mailto unblocks procurement conversations. - cta: { label: 'Contact sales →', href: 'mailto:enterprise@instanode.dev?subject=Team%20tier%20inquiry', variant: 'primary' }, + tagline: 'For the engineering org. Coming soon.', + price: 'coming soon', + freq: '', + features: [], + cta: { label: '', href: '', variant: 'primary' }, + comingSoon: true, }, ] diff --git a/src/pages/PricingPage.tsx b/src/pages/PricingPage.tsx index 9f0ae82..2280cb9 100644 --- a/src/pages/PricingPage.tsx +++ b/src/pages/PricingPage.tsx @@ -9,8 +9,13 @@ import { copyToClipboard } from '../components/Common' // TierKey — the local-to-this-page tier enum. Marketing /pricing uses // `anonymous` (the public-facing label for the no-signup free curl tier), // not `free` — the marketing CTA goes through the agent flow rather than -// the dashboard signup. W11 inserted `hobby_plus` between hobby and pro. -type TierKey = 'anonymous' | 'hobby' | 'hobby_plus' | 'pro' | 'team' +// the dashboard signup. +// +// 2026-05-15: `hobby_plus` removed from this enum to keep the marketing +// surface as Anonymous / Hobby / Pro / Team. Hobby Plus is still a real +// paid tier in api/plans.yaml — it's reached via dashboard upsell flows +// (quota_wall, custom_domain prompts), not the public pricing ladder. +type TierKey = 'anonymous' | 'hobby' | 'pro' | 'team' // P2: monthly vs yearly pricing. The toggle on this page is purely // presentational — the CTA passes the chosen cycle through as @@ -58,19 +63,11 @@ const TIERS: { ctaHrefMonthly: '/app/checkout?plan=hobby&frequency=monthly', ctaHrefYearly: '/app/checkout?plan=hobby&frequency=yearly', }, - // hobby_plus (W11) — $19/mo mid-step. Sits between Hobby and Pro to - // anchor the decoy effect: research shows triple-tier $9/$19/$49 lifts - // conversion ~22% vs $9/$49. The CTA points at /checkout which routes - // to /api/v1/billing/checkout with plan=hobby_plus. - { - key: 'hobby_plus', - name: 'Hobby Plus', - monthly: { price: '$19', sub: '/ mo' }, - yearly: { price: '$16.58', sub: '/ mo billed yearly', saveLabel: 'save $29/yr' }, - cta: 'Start hobby plus →', - ctaHrefMonthly: '/app/checkout?plan=hobby_plus&frequency=monthly', - ctaHrefYearly: '/app/checkout?plan=hobby_plus&frequency=yearly', - }, + // 2026-05-15: Hobby Plus tile removed from the marketing matrix. + // The tier still exists in plans.yaml and is offered via dashboard + // upsell flows (quota wall, custom-domain prompts) — it's an + // internal step, not a public funnel entry. Re-insert here if the + // tier ever becomes a primary acquisition surface. { key: 'pro', name: 'Pro', @@ -81,24 +78,19 @@ const TIERS: { ctaHrefYearly: '/app/checkout?plan=pro&frequency=yearly', highlighted: true, }, - // Team tier is under active development — visible so customers can see the - // roadmap but disabled (no checkout, no signup). Backend k8s plumbing for - // team-scale dedicated infra is already in place; what's pending is the - // multi-seat + RBAC + SSO surface. + // Team tier — not launched yet. We intentionally show NO pricing, NO + // detail, NO CTA. A single quiet "coming soon" placeholder is enough to + // signal the tier exists without making any commitment customers can + // hold us to. The per-row SOON markers in the matrix below render as + // empty cells for the same reason. { key: 'team', name: 'Team', - monthly: { price: '$199', sub: '/ mo' }, - yearly: { price: '$165.83', sub: '/ mo billed yearly', saveLabel: 'save $398/yr' }, - // Self-serve checkout for Team is still being wired (multi-seat / RBAC - // UI). The Razorpay yearly plan and dedicated-infra k8s plumbing both - // exist, so the tier is sellable via enterprise@ today. Dropping - // `comingSoon` so the CTA renders as a clickable mailto link instead of - // a disabled span; per-feature SOON markers in the matrix below still - // flag the specific gaps (SSO, RBAC, audit-export) honestly. - cta: 'Contact sales →', - ctaHrefMonthly: 'mailto:enterprise@instanode.dev?subject=Team%20tier%20inquiry', - ctaHrefYearly: 'mailto:enterprise@instanode.dev?subject=Team%20tier%20annual%20inquiry', + monthly: { price: 'coming soon', sub: '' }, + // No yearly — there's nothing to bill annually for an unlaunched tier. + cta: '', + ctaHrefMonthly: '', + comingSoon: true, }, ] @@ -106,49 +98,50 @@ type Cell = | string | { mark: 'check' | 'dash' } | { text: string; comingSoon?: boolean } -// Row values are a 5-tuple in column order: Anonymous, Hobby, Hobby Plus, -// Pro, Team. The order MUST stay in lock-step with the TIERS array above. -type Row = { label: string; sub?: string; values: [Cell, Cell, Cell, Cell, Cell] } +// Row values are a 4-tuple in column order: Anonymous, Hobby, Pro, Team. +// (Hobby Plus removed from the marketing matrix on 2026-05-15 — it lives +// in plans.yaml + dashboard upsell flows only.) The order MUST stay in +// lock-step with the TIERS array above. +type Row = { label: string; sub?: string; values: [Cell, Cell, Cell, Cell] } // Team-tier values use { text: '', comingSoon: true } across the board because -// the tier isn't shipped — claiming "unlimited" for capacity we haven't -// delivered would be misleading. Once team launches, replace these with the -// real numbers from plans.yaml. +// the tier hasn't launched. Marketing copy keeps Team as "coming soon" +// until the launch ships; do not list capacity numbers here even if +// plans.yaml has them — the source of truth for what's advertised is +// the launch posture, not the registry. const SOON: Cell = { text: '', comingSoon: true } -// W11 (2026-05-13): added the Hobby Plus column (third position). Each row -// now has 5 cells: [Anonymous, Hobby, Hobby Plus, Pro, Team]. Numbers come -// from api/plans.yaml. Hobby Plus shares Postgres/Redis with Hobby and -// bumps MongoDB, storage, webhooks, deployments, and unlocks multi-env -// + custom domain + 1-click restore. +// Each row has 4 cells: [Anonymous, Hobby, Pro, Team]. Numbers come from +// api/plans.yaml. 2026-05-15: Hobby Plus column removed (the tier exists +// for upsell flows but is not part of the public ladder); Pro storage +// bumped per PRICING-AUDIT-2026-05-15.md (Postgres 5→10 GB, Redis +// 256→512 MB, Mongo 2→5 GB, object 10→50 GB). const ROWS: Row[] = [ - { label: 'Postgres', values: ['10 MB / 2 conn / 24h TTL', '1 GB / 8 conn', '1 GB / 8 conn', '5 GB / 20 conn', SOON] }, - { label: 'Redis', values: ['5 MB / 24h TTL', '50 MB', '50 MB', '256 MB', SOON] }, - { label: 'MongoDB', values: ['5 MB / 2 conn / 24h TTL', '100 MB / 5 conn', '1 GB / 5 conn', '2 GB / 20 conn', SOON] }, + { label: 'Postgres', values: ['10 MB / 2 conn / 24h TTL', '1 GB / 8 conn', '10 GB / 20 conn', SOON] }, + { label: 'Redis', values: ['5 MB / 24h TTL', '50 MB', '512 MB', SOON] }, + { label: 'MongoDB', values: ['5 MB / 2 conn / 24h TTL', '100 MB / 5 conn', '5 GB / 20 conn', SOON] }, // FIX-G (2026-05-14): the column used to advertise "1 000 / 5 000 / 100k // msg/d" but there's no backing queue_messages_per_day field on the // plans.yaml side — quota enforcement is on queue_storage_mb. Shipping // a per-day-msg counter is real scope and not in flight, so the copy // moves to the field we actually enforce. Numbers mirror plans.yaml - // queue_storage_mb (anonymous=1024, hobby/hobby_plus=5120, pro=10240). - { label: 'Queue', sub: 'NATS storage', values: ['1 GB / 24h TTL', '5 GB', '5 GB', '10 GB', SOON] }, - { label: 'Storage', values: [{ mark: 'dash' }, '512 MB', '5 GB', '10 GB', SOON] }, - { label: 'Webhook stored', values: ['100', '1 000', '5 000', '10k', SOON] }, - { label: 'Deploy apps', values: [{ mark: 'dash' }, '1 small', '2 medium', '10 medium', SOON] }, - { label: 'Domains', values: [{ mark: 'dash' }, '*.deployment.instanode.dev', 'custom domain', 'custom domain', SOON] }, + // queue_storage_mb (anonymous=1024, hobby=5120, pro=10240). + { label: 'Queue', sub: 'NATS storage', values: ['1 GB / 24h TTL', '5 GB', '10 GB', SOON] }, + { label: 'Storage', values: [{ mark: 'dash' }, '512 MB', '50 GB', SOON] }, + { label: 'Webhook stored', values: ['100', '1 000', '10k', SOON] }, + { label: 'Deploy apps', values: [{ mark: 'dash' }, '1 small', '10 medium', SOON] }, + { label: 'Domains', values: [{ mark: 'dash' }, '*.deployment.instanode.dev', 'custom domain', SOON] }, // Multi-env workflows (stack promotion + vault copy across envs) is a // shipped Pro-tier feature: POST /api/v1/stacks/:slug/promote and // POST /api/v1/vault/copy are live (RETRO-2026-05-12 §10.17). Hobby is - // single-env (production only). W11: Hobby Plus also unlocks multi-env - // — it's the marquee differentiator vs Hobby alongside custom domains - // and the 2-deployment cap bump. - { label: 'Multi-env workflows', sub: 'stack promotion + vault copy', values: [{ mark: 'dash' }, { mark: 'dash' }, 'dev / staging / prod', 'dev / staging / prod', SOON] }, - { label: 'RBAC + audit', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] }, - { label: 'Vault entries', values: [{ mark: 'dash' }, '20', '50', '200', SOON] }, - { label: 'Vault envs', values: [{ mark: 'dash' }, 'production only', 'multi-env', 'multi-env', SOON] }, - { label: 'Backups', values: [{ mark: 'dash' }, '7-day · no restore', '14-day · 1-click restore', '30-day · 1-click restore', SOON] }, - { label: 'SSO / SAML', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] }, - { label: '99.9% SLA', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] } + // single-env (production only). + { label: 'Multi-env workflows', sub: 'stack promotion + vault copy', values: [{ mark: 'dash' }, { mark: 'dash' }, 'dev / staging / prod', SOON] }, + { label: 'RBAC + audit', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] }, + { label: 'Vault entries', values: [{ mark: 'dash' }, '20', '200', SOON] }, + { label: 'Vault envs', values: [{ mark: 'dash' }, 'production only', 'multi-env', SOON] }, + { label: 'Backups', values: [{ mark: 'dash' }, '7-day · no restore', '30-day · 1-click restore', SOON] }, + { label: 'SSO / SAML', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] }, + { label: '99.9% SLA', values: [{ mark: 'dash' }, { mark: 'dash' }, { mark: 'dash' }, SOON] } ] const FAQ: { q: string; a: string }[] = [