diff --git a/website/.env.example b/website/.env.example
index 2a1ca550..36084942 100644
--- a/website/.env.example
+++ b/website/.env.example
@@ -57,3 +57,11 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
# no-ops when unset.
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
+
+# ---------------------------------------------------------------------------
+# EmailOctopus (newsletter signup)
+# ---------------------------------------------------------------------------
+# Create an API key at https://emailoctopus.com/api-keys and grab the list
+# ID from the list URL. /api/subscribe returns 501 until both are set.
+EMAIL_OCTOPUS_API_KEY=
+EMAIL_OCTOPUS_LIST_ID=
diff --git a/website/src/app/api/subscribe/route.ts b/website/src/app/api/subscribe/route.ts
new file mode 100644
index 00000000..9023f733
--- /dev/null
+++ b/website/src/app/api/subscribe/route.ts
@@ -0,0 +1,38 @@
+import { type NextRequest, NextResponse } from "next/server"
+import { isEmailOctopusConfigured, subscribeToList } from "@/lib/email-octopus"
+import { withCapture } from "@/lib/telemetry/posthog-server"
+
+type Body = { email?: unknown }
+
+export const POST = withCapture(async (request: NextRequest) => {
+ if (!isEmailOctopusConfigured()) {
+ return NextResponse.json({ error: "not_configured" }, { status: 501 })
+ }
+
+ let body: Body
+ try {
+ body = (await request.json()) as Body
+ } catch {
+ return NextResponse.json({ error: "invalid_json" }, { status: 400 })
+ }
+
+ if (typeof body.email !== "string") {
+ return NextResponse.json({ error: "missing_email" }, { status: 400 })
+ }
+
+ const result = await subscribeToList(body.email)
+ if (result.ok) {
+ return NextResponse.json({ ok: true })
+ }
+
+ switch (result.reason) {
+ case "invalid_email":
+ return NextResponse.json({ error: "invalid_email" }, { status: 400 })
+ case "already_subscribed":
+ return NextResponse.json({ ok: true, alreadySubscribed: true })
+ case "rate_limited":
+ return NextResponse.json({ error: "rate_limited" }, { status: 429 })
+ default:
+ return NextResponse.json({ error: "upstream_error" }, { status: 502 })
+ }
+}, "/api/subscribe")
diff --git a/website/src/app/join/page.tsx b/website/src/app/join/page.tsx
new file mode 100644
index 00000000..587a4c1c
--- /dev/null
+++ b/website/src/app/join/page.tsx
@@ -0,0 +1,145 @@
+import type { Metadata } from "next"
+import Link from "next/link"
+import { ArrowLeft, Mic, RadioTower, ShieldCheck } from "lucide-react"
+import { BrandWordmark } from "@/components/brand/brand-system"
+import { ThemeSwitcher } from "@/components/theme-switcher"
+import { JoinForm } from "@/components/join/join-form"
+
+const REPO_URL = "https://github.com/yagudaev/voiceclaw"
+const RELEASES_URL = "https://github.com/yagudaev/voiceclaw/releases"
+
+export const metadata: Metadata = {
+ title: "Join VoiceClaw",
+ description:
+ "Get launch notes, TestFlight invites, and build updates for VoiceClaw — the open-source voice layer for your agent.",
+}
+
+export default function JoinPage() {
+ return (
+
+
+
+
+
+ Join
+
+
+ Get on the list.
+
+
+ Drop your email and we'll send launch notes, TestFlight invites,
+ and the occasional build update. Voice for the agent you already
+ trust.
+
+
+
+
+
+
+
+ }
+ title="Natural voice in front"
+ body="Low-latency voice sessions with transcript continuity and clear live state."
+ />
+ }
+ title="iPhone TestFlight"
+ body="Members get TestFlight invites first while App Store review wraps up."
+ />
+ }
+ title="Open source"
+ body="Run the relay yourself, inspect the code, and keep provider keys under your control."
+ />
+
+
+
+ Already have a Mac?{" "}
+
+ Download the macOS app
+
+ .
+
+
+ )
+}
diff --git a/website/src/app/page.tsx b/website/src/app/page.tsx
index 0167a818..e125b626 100644
--- a/website/src/app/page.tsx
+++ b/website/src/app/page.tsx
@@ -3,6 +3,7 @@ import Link from "next/link"
import {
ArrowRight,
Download,
+ Mail,
Mic,
PlayCircle,
RadioTower,
@@ -27,6 +28,7 @@ import {
const REPO_URL = "https://github.com/yagudaev/voiceclaw"
const MAC_DOWNLOAD_URL = "/download"
+const JOIN_URL = "/join"
const TESTFLIGHT_SIGNUP_URL = ""
const DEMO_EMBED_URL = "https://www.youtube.com/embed/iAS7vj2vRaA"
const HERO_BARS = [26, 44, 58, 42, 24, 34, 52, 78, 50, 31, 66, 86, 60, 38, 54, 82, 48, 72]
@@ -71,14 +73,13 @@ function Header({
-
- Download Mac
+
+ Join
@@ -112,20 +113,29 @@ function HeroSection({
OpenAI-compatible endpoint and it handles the mic, the route, and
the transcript while your agent does the real work.
-
+
+
+
+ Join
+
Download for Mac
Watch demo
@@ -377,20 +387,30 @@ function GetStartedSection({
Get started
- Start with the Mac app.
+ Get on the list.
- Download VoiceClaw for macOS, connect your agent endpoint, and start
- talking.
+ Drop your email for launch notes, TestFlight invites, and build
+ updates. Already on a Mac? Grab the desktop app below.{" "}
+