Gate A: take invented prices off every public surface
/pricing and the homepage teaser rendered the $0/$79/Custom catalog from lib/billing.ts with a below-the-fold disclaimer. Board minutes #42 ratified Gate A: no price on a public surface until the model is ratified and the entitlements exist — a false price anchor with a footnote is worse than no price. The page has been live in breach since ratification (server-26#46). - /pricing: no numbers, no plan cards, no interval toggle. "Pricing is in development", CTA to the existing /waitlist request-access page. - homepage: pricing teaser replaced with the same message; PLANS import gone. - homepage CTAs pointed at /login, which has no signup path — a real visitor could not create an account. Now /waitlist ("Request access"); the secondary CTA is honestly labelled "Sign in". - lib/billing.ts: plan catalog header now states the prices are invented and that retention/SSO/SLA have no backend, so the next person to import PLANS is warned at the definition site. Refs server-26#46, server-26#62. Authenticated /settings/billing is unchanged and still stubbed — not a public price surface, stays with #46. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cc038e6326
commit
a1bdccff45
+12
-23
@@ -4,7 +4,6 @@ import Link from "next/link";
|
|||||||
import { LinkButton } from "@/components/ui/Button";
|
import { LinkButton } from "@/components/ui/Button";
|
||||||
import { Card } from "@/components/ui/Card";
|
import { Card } from "@/components/ui/Card";
|
||||||
import { Badge } from "@/components/ui/Badge";
|
import { Badge } from "@/components/ui/Badge";
|
||||||
import { PLANS } from "@/lib/billing";
|
|
||||||
import { useAuth } from "@/components/AuthProvider";
|
import { useAuth } from "@/components/AuthProvider";
|
||||||
import { LiveView } from "@/components/LiveView";
|
import { LiveView } from "@/components/LiveView";
|
||||||
|
|
||||||
@@ -58,8 +57,8 @@ function MarketingHomePage() {
|
|||||||
bot to relay the audio live to your team.
|
bot to relay the audio live to your team.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-8">
|
<div className="flex flex-wrap items-center gap-3 mt-8">
|
||||||
<LinkButton href="/login" size="lg">Get started</LinkButton>
|
<LinkButton href="/waitlist" size="lg">Request access</LinkButton>
|
||||||
<LinkButton href="/pricing" variant="secondary" size="lg">View pricing</LinkButton>
|
<LinkButton href="/login" variant="secondary" size="lg">Sign in</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,31 +99,21 @@ function MarketingHomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Pricing teaser */}
|
{/* Pricing — Gate A (minutes #42/#62): no price on a public surface. */}
|
||||||
<section className="border-t border-gray-800">
|
<section className="border-t border-gray-800">
|
||||||
<div className="max-w-screen-xl mx-auto px-4 md:px-6 py-16 md:py-20">
|
<div className="max-w-screen-xl mx-auto px-4 md:px-6 py-16 md:py-20">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 mb-10">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-display-sm text-white">Plans for one node or a whole region</h2>
|
<h2 className="text-display-sm text-white">Pricing is in development</h2>
|
||||||
<p className="text-gray-400 mt-2">Start free. Upgrade when you add nodes or need longer retention.</p>
|
<p className="text-gray-400 mt-2 max-w-xl">
|
||||||
|
We would rather talk to you about what you need than post a number we would have to
|
||||||
|
walk back. Tell us about your coverage area and we will figure it out together.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/pricing" className="text-indigo-400 hover:text-indigo-300 text-sm font-mono transition-colors shrink-0">
|
<Link href="/pricing" className="text-indigo-400 hover:text-indigo-300 text-sm font-mono transition-colors shrink-0">
|
||||||
See full plan comparison →
|
More on pricing →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
|
||||||
{PLANS.map((plan) => (
|
|
||||||
<Card key={plan.id} padding="lg" highlighted={plan.highlighted}>
|
|
||||||
{plan.highlighted && <Badge tone="brand" className="mb-3">Most popular</Badge>}
|
|
||||||
<h3 className="text-white font-semibold">{plan.name}</h3>
|
|
||||||
<p className="text-gray-500 text-xs mt-1">{plan.tagline}</p>
|
|
||||||
<p className="text-white text-2xl font-bold font-mono mt-4">
|
|
||||||
{plan.priceMonthlyUsd === null ? "Custom" : plan.priceMonthlyUsd === 0 ? "Free" : `$${plan.priceMonthlyUsd}`}
|
|
||||||
{plan.priceMonthlyUsd !== null && plan.priceMonthlyUsd > 0 && <span className="text-gray-500 text-sm font-normal">/mo</span>}
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -133,10 +122,10 @@ function MarketingHomePage() {
|
|||||||
<div className="max-w-screen-xl mx-auto px-4 md:px-6 py-16 md:py-20 text-center">
|
<div className="max-w-screen-xl mx-auto px-4 md:px-6 py-16 md:py-20 text-center">
|
||||||
<h2 className="text-display-sm text-white">Bring your first node online</h2>
|
<h2 className="text-display-sm text-white">Bring your first node online</h2>
|
||||||
<p className="text-gray-400 mt-3 max-w-xl mx-auto">
|
<p className="text-gray-400 mt-3 max-w-xl mx-auto">
|
||||||
Sign in to create an account, add a node, and start seeing incidents within minutes of your first call.
|
Tell us about your coverage area. We will get you a node online and you will see incidents within minutes of your first call.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<LinkButton href="/login" size="lg">Get started</LinkButton>
|
<LinkButton href="/waitlist" size="lg">Request access</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,99 +1,45 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PLANS, type BillingInterval } from "@/lib/billing";
|
|
||||||
import { Card } from "@/components/ui/Card";
|
import { Card } from "@/components/ui/Card";
|
||||||
import { Badge } from "@/components/ui/Badge";
|
|
||||||
import { LinkButton } from "@/components/ui/Button";
|
import { LinkButton } from "@/components/ui/Button";
|
||||||
|
|
||||||
function CheckIcon() {
|
/**
|
||||||
return (
|
* Gate A (BUSINESS_MODEL.md, board minutes #42, enforced by minutes #62):
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" className="text-green-400 shrink-0 mt-0.5">
|
* no price may appear on a public surface until the pricing model is ratified
|
||||||
<polyline points="20 6 9 17 4 12" />
|
* and the entitlements behind it exist. The previous version of this page
|
||||||
</svg>
|
* rendered the invented $0/$79/Custom catalog from lib/billing.ts with a
|
||||||
);
|
* below-the-fold disclaimer — a false price anchor with a footnote is worse
|
||||||
}
|
* than no price. Do not re-import PLANS here. Tracked: server-26#46.
|
||||||
|
*/
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const [interval, setInterval] = useState<BillingInterval>("monthly");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-screen-xl mx-auto px-4 md:px-6 py-16 md:py-20">
|
<div className="max-w-screen-xl mx-auto px-4 md:px-6 py-16 md:py-20">
|
||||||
<div className="text-center max-w-2xl mx-auto">
|
<div className="text-center max-w-2xl mx-auto">
|
||||||
<h1 className="text-display-sm md:text-display text-white">Simple, node-based pricing</h1>
|
<h1 className="text-display-sm md:text-display text-white">Pricing is in development</h1>
|
||||||
<p className="text-gray-400 mt-4">
|
<p className="text-gray-400 mt-4">
|
||||||
Every plan includes the full incident pipeline — transcription, correlation, mapping, and the Discord relay.
|
We are still working out what a fair price looks like for the people who actually use this.
|
||||||
Plans differ in how many nodes and seats you get, and how far back your history goes.
|
Rather than post a number we would have to walk back, we would rather talk to you about what
|
||||||
|
you need and what it is worth.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Interval toggle */}
|
<Card padding="lg" className="mt-12 max-w-2xl mx-auto">
|
||||||
<div className="flex items-center justify-center gap-1 mt-10 bg-gray-900 border border-gray-800 rounded-lg p-1 w-fit mx-auto">
|
<h2 className="text-white text-lg font-bold">What you get today</h2>
|
||||||
{(["monthly", "annual"] as BillingInterval[]).map((i) => (
|
<p className="text-gray-400 text-sm mt-2 leading-relaxed">
|
||||||
<button
|
The full incident pipeline — transcription, correlation into incidents, mapping, and the
|
||||||
key={i}
|
Discord relay. Node counts, seats, and history length are set per account while we work out
|
||||||
onClick={() => setInterval(i)}
|
the plan structure.
|
||||||
className={`text-sm font-mono px-4 py-1.5 rounded-md transition-colors capitalize ${
|
</p>
|
||||||
interval === i ? "bg-gray-800 text-white" : "text-gray-500 hover:text-gray-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{i}
|
|
||||||
{i === "annual" && <span className="ml-1.5 text-green-400 text-xs">save ~17%</span>}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Plan cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-10 items-stretch">
|
|
||||||
{PLANS.map((plan) => {
|
|
||||||
const price = interval === "annual" ? plan.priceAnnualUsd : plan.priceMonthlyUsd;
|
|
||||||
const priceLabel =
|
|
||||||
price === null ? "Custom" : price === 0 ? "Free" : `$${interval === "annual" ? Math.round(price / 12) : price}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card key={plan.id} padding="lg" highlighted={plan.highlighted} className="flex flex-col">
|
|
||||||
{plan.highlighted && <Badge tone="brand" className="mb-3 w-fit">Most popular</Badge>}
|
|
||||||
<h2 className="text-white text-lg font-bold">{plan.name}</h2>
|
|
||||||
<p className="text-gray-500 text-sm mt-1.5 leading-relaxed">{plan.tagline}</p>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<span className="text-white text-3xl font-bold font-mono">{priceLabel}</span>
|
<LinkButton href="/waitlist" fullWidth>Request access</LinkButton>
|
||||||
{price !== null && price > 0 && <span className="text-gray-500 text-sm">/mo</span>}
|
|
||||||
{interval === "annual" && price !== null && price > 0 && (
|
|
||||||
<p className="text-gray-600 text-xs mt-1">billed ${plan.priceAnnualUsd}/year</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<LinkButton href="/login" variant={plan.highlighted ? "primary" : "secondary"} fullWidth>
|
|
||||||
{plan.priceMonthlyUsd === null ? "Contact sales" : "Get started"}
|
|
||||||
</LinkButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="mt-6 space-y-2.5 flex-1">
|
|
||||||
{plan.features.map((f) => (
|
|
||||||
<li key={f} className="flex items-start gap-2 text-sm text-gray-300">
|
|
||||||
<CheckIcon />
|
|
||||||
{f}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-center text-gray-600 text-xs font-mono mt-8">
|
|
||||||
Prices shown are sample figures for this demo build — nothing here is connected to a live payment processor.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="text-center mt-16">
|
<div className="text-center mt-16">
|
||||||
<p className="text-gray-400">
|
<p className="text-gray-400">
|
||||||
Questions about a plan?{" "}
|
Questions?{" "}
|
||||||
<Link href="/faq" className="text-indigo-400 hover:text-indigo-300 transition-colors">Check the FAQ</Link>
|
<Link href="/faq" className="text-indigo-400 hover:text-indigo-300 transition-colors">Check the FAQ</Link>
|
||||||
{" "}or{" "}
|
{" "}or{" "}
|
||||||
<Link href="/login" className="text-indigo-400 hover:text-indigo-300 transition-colors">sign in to talk to us</Link>.
|
<Link href="/waitlist" className="text-indigo-400 hover:text-indigo-300 transition-colors">get in touch</Link>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,10 +72,26 @@ export interface UsageSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Plan catalog — this is real UI copy (safe to ship), just not wired to a
|
// Plan catalog — NOT SAFE TO SHIP. Do not put these on a public surface.
|
||||||
// live pricing table. In a real integration this would likely be fetched
|
//
|
||||||
// from the processor (Stripe Prices API) instead of hardcoded here so price
|
// These prices are INVENTED. BUSINESS_MODEL.md §3.1 (ratified in structure by
|
||||||
// changes don't require a frontend deploy.
|
// board minutes #42, 2026-08-23) marks $0/$79/custom as superseded, and the
|
||||||
|
// board ruled that pricing comes OFF the public site entirely — replaced with
|
||||||
|
// "Pricing in development — contact us" — rather than kept behind a
|
||||||
|
// below-the-fold disclaimer. A false price anchor with a footnote is worse
|
||||||
|
// than no price. Tracked in server-26#46 (Gate A, due 2026-09-13).
|
||||||
|
//
|
||||||
|
// Also unbacked by any implementation, and each is its own claim-vs-reality
|
||||||
|
// gap if displayed:
|
||||||
|
// - retentionDays 7/90/365 — no TTL and no deletion sweep exists anywhere
|
||||||
|
// in drb-c2-core. server-26#44 (Gate B4).
|
||||||
|
// - SSO/SAML, uptime SLA, custom data residency — no backend at all.
|
||||||
|
// There are also zero backend billing routes; createCheckoutSession() and
|
||||||
|
// createBillingPortalSession() always throw. See ADMIN_BILLING_AUDIT.md §2.
|
||||||
|
//
|
||||||
|
// If pricing is ever wired for real, fetch it from the processor (Stripe
|
||||||
|
// Prices API) rather than hardcoding here, so price changes don't require a
|
||||||
|
// frontend deploy.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const PLANS: PlanDefinition[] = [
|
export const PLANS: PlanDefinition[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user