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 { Card } from "@/components/ui/Card";
import { Badge } from "@/components/ui/Badge";
import { PLANS } from "@/lib/billing";
import { useAuth } from "@/components/AuthProvider";
import { LiveView } from "@/components/LiveView";
@@ -58,8 +57,8 @@ function MarketingHomePage() {
bot to relay the audio live to your team.
</p>
<div className="flex flex-wrap items-center gap-3 mt-8">
<LinkButton href="/login" size="lg">Get started</LinkButton>
<LinkButton href="/pricing" variant="secondary" size="lg">View pricing</LinkButton>
<LinkButton href="/waitlist" size="lg">Request access</LinkButton>
<LinkButton href="/login" variant="secondary" size="lg">Sign in</LinkButton>
</div>
</div>
</div>
@@ -100,31 +99,21 @@ function MarketingHomePage() {
</div>
</section>
{/* Pricing teaser */}
{/* Pricing — Gate A (minutes #42/#62): no price on a public surface. */}
<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="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>
<h2 className="text-display-sm text-white">Plans for one node or a whole region</h2>
<p className="text-gray-400 mt-2">Start free. Upgrade when you add nodes or need longer retention.</p>
<h2 className="text-display-sm text-white">Pricing is in development</h2>
<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>
<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>
</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>
</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">
<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">
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>
<div className="mt-8">
<LinkButton href="/login" size="lg">Get started</LinkButton>
<LinkButton href="/waitlist" size="lg">Request access</LinkButton>
</div>
</div>
</section>
+22 -76
View File
@@ -1,99 +1,45 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { PLANS, type BillingInterval } from "@/lib/billing";
import { Card } from "@/components/ui/Card";
import { Badge } from "@/components/ui/Badge";
import { LinkButton } from "@/components/ui/Button";
function CheckIcon() {
return (
<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">
<polyline points="20 6 9 17 4 12" />
</svg>
);
}
/**
* Gate A (BUSINESS_MODEL.md, board minutes #42, enforced by minutes #62):
* no price may appear on a public surface until the pricing model is ratified
* and the entitlements behind it exist. The previous version of this page
* 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() {
const [interval, setInterval] = useState<BillingInterval>("monthly");
return (
<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">
<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">
Every plan includes the full incident pipeline — transcription, correlation, mapping, and the Discord relay.
Plans differ in how many nodes and seats you get, and how far back your history goes.
We are still working out what a fair price looks like for the people who actually use this.
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>
</div>
{/* Interval toggle */}
<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">
{(["monthly", "annual"] as BillingInterval[]).map((i) => (
<button
key={i}
onClick={() => setInterval(i)}
className={`text-sm font-mono px-4 py-1.5 rounded-md transition-colors capitalize ${
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>
<Card padding="lg" className="mt-12 max-w-2xl mx-auto">
<h2 className="text-white text-lg font-bold">What you get today</h2>
<p className="text-gray-400 text-sm mt-2 leading-relaxed">
The full incident pipeline — transcription, correlation into incidents, mapping, and the
Discord relay. Node counts, seats, and history length are set per account while we work out
the plan structure.
</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>
)}
<LinkButton href="/waitlist" fullWidth>Request access</LinkButton>
</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">
<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>
{" "}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>
</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
// live pricing table. In a real integration this would likely be fetched
// from the processor (Stripe Prices API) instead of hardcoded here so price
// changes don't require a frontend deploy.
// Plan catalog — NOT SAFE TO SHIP. Do not put these on a public surface.
//
// These prices are INVENTED. BUSINESS_MODEL.md §3.1 (ratified in structure by
// 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[] = [