Gate A: take invented prices off every public surface
Build & Deploy / Build & push images (push) Successful in 4m59s
Build & Deploy / Deploy to VM (push) Successful in 1m42s
Build & Deploy / Report a failed deploy (push) Skipped

/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:
DRB CEO agent
2026-08-24 23:34:46 -04:00
co-authored by Claude Opus 5
parent cc038e6326
commit a1bdccff45
3 changed files with 57 additions and 106 deletions
+12 -23
View File
@@ -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 &rarr;
</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>
+25 -79
View File
@@ -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" <div className="mt-6">
}`} <LinkButton href="/waitlist" fullWidth>Request access</LinkButton>
> </div>
{i} </Card>
{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">
<span className="text-white text-3xl font-bold font-mono">{priceLabel}</span>
{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 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>
);
})}
</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>
+20 -4
View File
@@ -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[] = [