diff --git a/drb-frontend/app/privacy/page.tsx b/drb-frontend/app/privacy/page.tsx
new file mode 100644
index 0000000..1abf63e
--- /dev/null
+++ b/drb-frontend/app/privacy/page.tsx
@@ -0,0 +1,83 @@
+import Link from "next/link";
+
+/**
+ * SAAS_PLAN.md B5: page structure only — see app/terms/page.tsx for why the
+ * agent building this did not write real legal text. Privacy Policy needs
+ * the same jurisdiction-aware legal review as Terms, plus specifics this
+ * agent cannot respond for on the owner's behalf: what a real DPA/CCPA/GDPR
+ * posture looks like, and what third-party processors (OpenAI, Gemini,
+ * Google Maps, Firebase/GCP, Stripe once chosen) actually receive and why.
+ */
+
+const SECTIONS: { heading: string; note: string }[] = [
+ {
+ heading: "1. What data this collects",
+ note: "TODO(legal): account data (email, org membership), field node telemetry (location, status), radio call audio and AI-generated transcripts/entities/incident data, and usage/session logs (drb-c2-core's audit_log and user_sessions collections already exist and hold some of this today).",
+ },
+ {
+ heading: "2. Third parties this data is sent to, and why",
+ note: "TODO(legal): OpenAI (Whisper transcription), Google Gemini (incident extraction/summarization/embeddings), Google Maps (geocoding location strings extracted from transcripts), Google Cloud (Firestore + GCS storage, Firebase Auth), and — once a payment processor is chosen (SAAS_PLAN.md section 6.5, not yet decided) — that processor. Each of these is a real, already-integrated dependency, not a hypothetical one; this section needs to name them accurately, not generically.",
+ },
+ {
+ heading: "3. Recorded radio traffic specifically",
+ note: "TODO(legal): this product's core function is recording, transcribing, and storing monitored radio audio — including public-safety traffic that may name individuals, locations, and in-progress incidents. This needs explicit treatment distinct from generic 'we collect usage data' privacy boilerplate, and needs to be read alongside the same legal review flagged in Terms section 3.",
+ },
+ {
+ heading: "4. How long data is kept",
+ note: "TODO(legal): no retention enforcement exists in the product yet (no TTL, no sweep, no deletion job — see DEFERRED.md) — this section cannot promise a retention/deletion window the system doesn't actually implement.",
+ },
+ {
+ heading: "5. Customer and end-user rights",
+ note: "TODO(legal): access/export/deletion requests, and who they're directed to — org owner vs. platform operator.",
+ },
+ {
+ heading: "6. Cookies and session data",
+ note: "TODO(legal): drb_session is a client-set, non-httpOnly cookie used only for UI redirect logic (not an auth boundary — see CLAUDE.md); Firebase Auth sets its own session storage. No analytics/tracking cookies are set today.",
+ },
+ {
+ heading: "7. Security practices",
+ note: "TODO(legal): at a level appropriate for public disclosure — Firestore security rules, per-node credentials, encrypted transport. Should be reviewed against SAAS_PLAN.md's actual findings before publishing any specific claim.",
+ },
+ {
+ heading: "8. Changes to this policy",
+ note: "TODO(legal): how customers are notified.",
+ },
+ {
+ heading: "9. Contact",
+ note: "TODO(legal): real company legal identity and contact address — not yet decided (SAAS_PLAN.md section 6.6).",
+ },
+];
+
+export default function PrivacyPage() {
+ return (
+
+
+
+ Draft — not yet in force
+
+
+ This page is a structural placeholder, not a real Privacy Policy. Every section below is a{" "}
+ TODO(legal) marker, not actual legal text. Nothing on this page
+ describes a binding commitment about how data is handled.
+
+
+
+
Privacy Policy
+
Draft — last structured {new Date().getFullYear()}
+
+
+ {SECTIONS.map((s) => (
+
+
{s.heading}
+
{s.note}
+
+ ))}
+
+
+
+ Questions in the meantime? Check the FAQ or{" "}
+ sign in to reach us directly.
+
+
+ );
+}
diff --git a/drb-frontend/app/terms/page.tsx b/drb-frontend/app/terms/page.tsx
new file mode 100644
index 0000000..82a3f32
--- /dev/null
+++ b/drb-frontend/app/terms/page.tsx
@@ -0,0 +1,89 @@
+import Link from "next/link";
+
+/**
+ * SAAS_PLAN.md B5: page structure only. The agent building this is
+ * explicitly instructed not to invent legal text — DRB records, stores, and
+ * transcribes public-safety radio traffic, and recording/rebroadcast
+ * legality varies by state (see SAAS_PLAN.md section 6.3), so this needs a
+ * human, and probably a lawyer, not a generated draft. Every section below
+ * is a placeholder marking what a real Terms of Service needs to cover, not
+ * actual terms — see the banner and every TODO(legal) marker.
+ */
+
+const SECTIONS: { heading: string; note: string }[] = [
+ {
+ heading: "1. Acceptance of terms",
+ note: "TODO(legal): standard acceptance clause — using the service means agreeing to these terms.",
+ },
+ {
+ heading: "2. What the service does and does not do",
+ note: "TODO(legal): describe the product (SDR ingestion, transcription, AI correlation, Discord relay) and, importantly, disclaim accuracy — AI-generated transcripts and incident summaries are not guaranteed accurate and must not be relied on as the sole source for dispatch or safety decisions.",
+ },
+ {
+ heading: "3. Radio recording and rebroadcast — the part that needs a lawyer",
+ note: "TODO(legal): this is the section that actually matters. Recording, storing, and rebroadcasting monitored radio traffic (including public-safety frequencies) has different legal treatment by state and by traffic type (encrypted vs. clear, dispatch vs. tactical). Needs jurisdiction-aware legal review before this product can be sold across state lines — do not ship this page live without it.",
+ },
+ {
+ heading: "4. Customer responsibilities and acceptable use",
+ note: "TODO(legal): who owns the hardware, who's responsible for lawful operation of the field node, prohibited uses.",
+ },
+ {
+ heading: "5. Data ownership and retention",
+ note: "TODO(legal): who owns the recorded audio/transcripts/incidents, how long they're kept, what happens on cancellation or account deletion. Note: no retention enforcement exists in the product yet either (see DEFERRED.md) — this section can't promise a retention window the system doesn't yet enforce.",
+ },
+ {
+ heading: "6. Payment, billing, and cancellation",
+ note: "TODO(legal): once a billing model and pricing exist (SAAS_PLAN.md section 6, still undecided) — refunds, proration, what happens to data on non-payment.",
+ },
+ {
+ heading: "7. Service availability and support",
+ note: "TODO(legal): whether any uptime/SLA commitment is made (today: none).",
+ },
+ {
+ heading: "8. Limitation of liability",
+ note: "TODO(legal): standard limitation-of-liability language, reviewed against the fact that this product touches public-safety-adjacent data.",
+ },
+ {
+ heading: "9. Changes to these terms",
+ note: "TODO(legal): how customers are notified of material changes.",
+ },
+ {
+ heading: "10. Governing law and contact",
+ note: "TODO(legal): governing jurisdiction, and a real company legal identity and contact address (SAAS_PLAN.md section 6.6 — not yet decided).",
+ },
+];
+
+export default function TermsPage() {
+ return (
+
+
+
+ Draft — not yet in force
+
+
+ This page is a structural placeholder, not a real Terms of Service. Every section below is a{" "}
+ TODO(legal) marker, not actual legal text. Nothing on this page is
+ binding, and no self-serve signup should be considered subject to it until an actual attorney-reviewed
+ version replaces this content.
+
+
+
+
Terms of Service
+
Draft — last structured {new Date().getFullYear()}
+
+
+ {SECTIONS.map((s) => (
+
+
{s.heading}
+
{s.note}
+
+ ))}
+
+
+
+ Questions in the meantime? Check the FAQ or{" "}
+ sign in to reach us directly.
+
+
+ );
+}
diff --git a/drb-frontend/app/waitlist/page.tsx b/drb-frontend/app/waitlist/page.tsx
new file mode 100644
index 0000000..e089a41
--- /dev/null
+++ b/drb-frontend/app/waitlist/page.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { c2api } from "@/lib/c2api";
+
+/**
+ * SAAS_PLAN.md B6's waitlist form — POST /waitlist (routers/waitlist.py) is
+ * public, source-IP rate-limited, and deliberately not coupled to any plan
+ * or tier: the commercial model (who runs the node, what a customer
+ * actually buys) is still an open decision (SAAS_PLAN.md section 6.1), so
+ * this collects only {email, org_name, note} and makes no promise about
+ * price or plan.
+ */
+export default function WaitlistPage() {
+ const [email, setEmail] = useState("");
+ const [orgName, setOrgName] = useState("");
+ const [note, setNote] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+ const [done, setDone] = useState(false);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setSubmitting(true);
+ setError(null);
+ try {
+ await c2api.joinWaitlist({ email, org_name: orgName || undefined, note: note || undefined });
+ setDone(true);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Could not submit — try again in a moment.");
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+
+ D
+ DRB
+
+
+ {done ? (
+ <>
+
You're on the list
+
+ Thanks — we'll reach out at the email you gave us. In the meantime, feel free to{" "}
+ read the FAQ.
+
+ >
+ ) : (
+ <>
+
+
Request access
+
+ Self-serve signup isn't open yet. Leave your details and we'll follow up to get your organization set up.
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/drb-frontend/components/marketing/MarketingFooter.tsx b/drb-frontend/components/marketing/MarketingFooter.tsx
index bfad285..0cab54c 100644
--- a/drb-frontend/components/marketing/MarketingFooter.tsx
+++ b/drb-frontend/components/marketing/MarketingFooter.tsx
@@ -13,6 +13,9 @@ export function MarketingFooter() {
Features
Pricing
FAQ
+ Request access
+ Terms
+ Privacy
Sign in
diff --git a/drb-frontend/middleware.ts b/drb-frontend/middleware.ts
index 6cff905..bbe1f29 100644
--- a/drb-frontend/middleware.ts
+++ b/drb-frontend/middleware.ts
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
// Public marketing pages — no session required. Keep this in sync with
// MARKETING_PATHS in components/ChromeSwitcher.tsx (that one picks page
// chrome; this one decides whether to redirect at all).
-const PUBLIC_PATHS = new Set(["/", "/features", "/pricing", "/faq", "/terms", "/privacy"]);
+const PUBLIC_PATHS = new Set(["/", "/features", "/pricing", "/faq", "/terms", "/privacy", "/waitlist"]);
// /signup and /onboarding are deliberately NOT gated by the drb_session
// cookie here, even though they aren't "public" in the sense of not needing