feat(launch): waitlist page, raw-WebGPU hero, supabase + vercel infra

The July 14 launch surface, previously uncommitted:
- /dashboard/launch raw-WebGPU particle "memory brain" hero (RawVestigeEngine,
  NeuralWordmark, dendrite sign) + DOM waitlist overlay with share/referral.
- Supabase waitlist client + migrations + welcome edge function; legacy waitlist
  archived under supabase/legacy.
- vercel.json deploy config, root-redirect env wiring, base-path config,
  graph-only route, Playwright launch verifiers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-27 11:13:02 -05:00
parent 24ef2d504f
commit 6837198328
27 changed files with 5249 additions and 747 deletions

View file

@ -0,0 +1,37 @@
-- Vestige Pro waitlist capture.
--
-- One row per signup from the public /dashboard/waitlist form. The form POSTs
-- through the `waitlist-join` Edge Function (service-role), NOT directly to this
-- table, so Row-Level Security stays closed to anon clients while the function
-- can insert. We keep the table minimal and queryable for launch-day export.
create table if not exists public.waitlist (
id uuid primary key default gen_random_uuid(),
email text not null,
name text,
plan text, -- 'solo' | 'team' (free-form, not enforced)
priority text, -- 'sync' | 'team-memory' | 'postgres' | 'support'
notes text,
source text default 'vestige-pro-waitlist',
user_agent text,
-- Lowercased email for case-insensitive dedup. Generated so callers can't
-- desync it from `email`.
email_norm text generated always as (lower(trim(email))) stored,
created_at timestamptz not null default now()
);
-- One signup per email address. ON CONFLICT in the function turns a re-submit
-- into a friendly "already on the list" instead of a duplicate row or an error.
create unique index if not exists waitlist_email_norm_key
on public.waitlist (email_norm);
-- Newest-first export / dashboards.
create index if not exists waitlist_created_at_idx
on public.waitlist (created_at desc);
-- RLS ON, no anon policies: the anon/public key can do NOTHING here directly.
-- Only the service-role key used inside the Edge Function bypasses RLS to insert.
alter table public.waitlist enable row level security;
comment on table public.waitlist is
'Vestige Pro launch waitlist. Inserted only via the waitlist-join Edge Function (service-role). RLS blocks all anon access.';

21
supabase/legacy/README.md Normal file
View file

@ -0,0 +1,21 @@
# Legacy waitlist backend (archived)
These files backed the **old** `/dashboard/waitlist` page (Canvas2D grid +
support bot), which POSTed signups to the `waitlist-join` Edge Function using a
service-role insert.
The **live launch page** is `/dashboard/launch`. It uses a different, simpler
architecture: direct browser inserts with the public anon key + insert-only RLS,
plus `security definer` RPCs for the referral loop. See:
- Schema + RPCs: `supabase/migrations/0002_launch_waitlist.sql`
- Go-live runbook: `docs/launch/waitlist-setup.md`
- Welcome/share email: `supabase/functions/waitlist-welcome/`
Both schemas declared a table literally named `waitlist` with **incompatible**
shapes (this one is `uuid` PK with `name/plan/notes`; the launch one is `bigint`
PK with `referral_code/referred_by`). They cannot coexist in the same database
under that name. The launch schema is the source of truth as of 2026-06-26.
Kept for reference / in case the old page is ever revived (it would need its own
table name). Nothing in the app references `waitlist-join` anymore.

View file

@ -0,0 +1,146 @@
// Vestige Pro waitlist capture — Supabase Edge Function (Deno).
//
// The public /dashboard/waitlist form POSTs JSON here. This function:
// 1. Handles CORS preflight (the form is served from a different origin).
// 2. Validates the email and rejects honeypot/bot submissions.
// 3. Inserts one row per email into public.waitlist (dedup on re-submit).
// 4. Optionally sends a confirmation email via Resend.
//
// Secrets (set with `supabase secrets set`, NEVER committed):
// SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY — auto-injected by Supabase
// RESEND_API_KEY — optional; if unset, skip email
// WAITLIST_FROM_EMAIL — optional; defaults below
// WAITLIST_ALLOWED_ORIGIN — optional CORS allowlist (comma-sep)
import { createClient } from "jsr:@supabase/supabase-js@2";
const DEFAULT_FROM = "Vestige <waitlist@vestige.dev>";
function corsHeaders(origin: string | null): HeadersInit {
const allow = (Deno.env.get("WAITLIST_ALLOWED_ORIGIN") ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
// If an allowlist is configured and the origin isn't on it, fall back to the
// first allowed origin (so browsers see a definite, non-wildcard value).
const allowedOrigin =
allow.length === 0 ? "*" : (origin && allow.includes(origin) ? origin : allow[0]);
return {
"Access-Control-Allow-Origin": allowedOrigin,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "content-type",
"Content-Type": "application/json",
};
}
interface WaitlistPayload {
name?: string;
email?: string;
plan?: string;
priority?: string;
notes?: string;
source?: string;
companySite?: string; // honeypot — must be empty
}
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
async function sendConfirmation(email: string, name: string) {
const key = Deno.env.get("RESEND_API_KEY");
if (!key) return; // email is optional; capture still succeeds without it
const from = Deno.env.get("WAITLIST_FROM_EMAIL") ?? DEFAULT_FROM;
const greeting = name ? `Hi ${name},` : "Hi,";
try {
await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from,
to: email,
subject: "You're on the Vestige Pro list",
text:
`${greeting}\n\nYou're on the Vestige Pro early-access list. ` +
`We'll email you before launch with your invite.\n\n— The Vestige team`,
}),
});
} catch (_err) {
// Never fail the signup because the email provider hiccuped.
}
}
Deno.serve(async (req) => {
const origin = req.headers.get("origin");
const headers = corsHeaders(origin);
if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers });
}
if (req.method !== "POST") {
return new Response(JSON.stringify({ ok: false, error: "method_not_allowed" }), {
status: 405,
headers,
});
}
let body: WaitlistPayload;
try {
body = await req.json();
} catch {
return new Response(JSON.stringify({ ok: false, error: "invalid_json" }), {
status: 400,
headers,
});
}
// Honeypot: real users never fill this hidden field. Pretend success so bots
// get no signal, but write nothing.
if (body.companySite && body.companySite.trim()) {
return new Response(JSON.stringify({ ok: true, deduped: false }), { status: 200, headers });
}
const email = (body.email ?? "").trim();
if (!EMAIL_RE.test(email)) {
return new Response(JSON.stringify({ ok: false, error: "invalid_email" }), {
status: 400,
headers,
});
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
// Insert; on email conflict, treat as already-joined (idempotent, friendly).
const row = {
email,
name: (body.name ?? "").trim() || null,
plan: (body.plan ?? "").trim() || null,
priority: (body.priority ?? "").trim() || null,
notes: (body.notes ?? "").trim() || null,
source: (body.source ?? "vestige-pro-waitlist").trim(),
user_agent: req.headers.get("user-agent")?.slice(0, 400) ?? null,
};
const { error } = await supabase.from("waitlist").insert(row);
if (error) {
// 23505 = unique_violation on email_norm → already on the list.
if (error.code === "23505") {
return new Response(JSON.stringify({ ok: true, deduped: true }), { status: 200, headers });
}
console.error("waitlist insert failed:", error);
return new Response(JSON.stringify({ ok: false, error: "storage_failed" }), {
status: 500,
headers,
});
}
// Fire-and-forget confirmation; the signup is already committed.
await sendConfirmation(email, row.name ?? "");
return new Response(JSON.stringify({ ok: true, deduped: false }), { status: 200, headers });
});