mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
feat(landing): full-viewport GPU node-engine hero + waitlist + dashboard auth removal
The /launch landing page (bleeding-edge waitlist hero): - Full-viewport WebGL node engine (src/lib/hero/nodeEngine.ts): 40k GPU particles on a two-FBO GPUComputationRenderer running an 18s looping cinematic — particles stream in from the screen edges, slam together and EXPLODE at center, reform into a brain / graph constellation / neural lattice, dissolve back out, loop. Glowing additive particles + UnrealBloom, fills the whole screen edge to edge. - Key GPGPU fix: custom DataTexture shape targets read black in GPUComputationRenderer (three.js #15882), so shape targets are computed PROCEDURALLY in-shader from the per-particle seed. Position integrates raw per-frame velocity (Codrops pattern), texel-center reference attribute. - AmbientField (god-ray glow + parallax starfield), phantomBrain (deterministic seed-from-identity memory graph), LandingHero/neuralFlow (WebGPU Cinema-storm reuse + WebGL fallback, kept as alternates). Manifesto copy, seed input, email capture. No em-dashes. SSR off + prerender for the WebGL route. Waitlist backend (Supabase, owned data): - supabase/migrations/0001_waitlist.sql (RLS-locked table, dedup), Edge Function waitlist-join (CORS, validation, honeypot, dedup, Resend confirmation), setup doc. Dashboard auth removed (reverts the launch-polish auth wall that 401'd the dashboard against itself): mod.rs/state.rs/websocket.rs back to no-token, api.ts/websocket.ts/ +layout.svelte stripped of the token client. Pending-review UX + Memory PR gating kept. Research/spec docs under docs/launch/. Frontend typechecks 0/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
335a42f341
commit
cfe8d03d36
26 changed files with 3002 additions and 266 deletions
10
supabase/config.toml
Normal file
10
supabase/config.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Supabase project config for the Vestige Pro waitlist capture.
|
||||
# Link to a real project with: supabase link --project-ref <your-ref>
|
||||
# Then deploy: supabase db push && supabase functions deploy waitlist-join
|
||||
|
||||
project_id = "vestige-waitlist"
|
||||
|
||||
[functions.waitlist-join]
|
||||
# Public form posts here; no Supabase JWT required (we gate with the honeypot,
|
||||
# email validation, and the service-role insert inside the function).
|
||||
verify_jwt = false
|
||||
146
supabase/functions/waitlist-join/index.ts
Normal file
146
supabase/functions/waitlist-join/index.ts
Normal 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 });
|
||||
});
|
||||
37
supabase/migrations/0001_waitlist.sql
Normal file
37
supabase/migrations/0001_waitlist.sql
Normal 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.';
|
||||
Loading…
Add table
Add a link
Reference in a new issue