From a2f4e459e143e0d2c4184a5334c8c4dc82e5c9f0 Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Sat, 27 Jun 2026 11:13:10 -0500 Subject: [PATCH] docs(launch): viral asset package + waitlist setup runbook - VIRAL_ASSETS_2026-07-14.md: X thread, Show HN, Reddit variants, 30s demo script, positioning. Firewall claim cleared after the re-audit confirmed enforcement; honest wording (kept-from-influencing, not "deleted"). - waitlist-setup.md refreshed. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/launch/VIRAL_ASSETS_2026-07-14.md | 402 +++++++++++++++++++++++++ docs/launch/waitlist-setup.md | 306 +++++++++++++++---- 2 files changed, 642 insertions(+), 66 deletions(-) create mode 100644 docs/launch/VIRAL_ASSETS_2026-07-14.md diff --git a/docs/launch/VIRAL_ASSETS_2026-07-14.md b/docs/launch/VIRAL_ASSETS_2026-07-14.md new file mode 100644 index 0000000..42f57ab --- /dev/null +++ b/docs/launch/VIRAL_ASSETS_2026-07-14.md @@ -0,0 +1,402 @@ +# Vestige Launch โ€” Viral Asset Package (2026-07-14) + +> CLAIM-GATE โ€” CLEARED 2026-06-27: The Microglial Firewall is now wired into the +> live write path (`gate_writes`) and a hostile re-audit confirmed both CRITICAL +> findings CLOSED: `screen_write` runs on every real write, and an integration +> test proves a screened injection is held out of that write's retrieval with the +> receipt carrying `influenceAllowed: false`. The firewall tweet (Tweet 6) and +> the demo clip are CLEARED to post/record. HONEST WORDING (required): say the +> blocked memory is "kept from influencing the answer" / `influenceAllowed: false` +> on the receipt โ€” do NOT claim it is "deleted" or "never retrievable again." +> Quarantine is an enforced retrieval-suppression for that answer, not erasure. +> (Open non-blocking follow-ups, task #11: close a leetspeak detector bypass; +> optional hard-exclusion flag.) Every other claim in this doc is true in shipped +> code. + +Distribution engine for the July 14, 2026 launch. Channels: X/Twitter thread, +Show HN, Reddit (r/LocalLLaMA, r/rust, r/ClaudeAI), 30s demo clip. + +Goal: npm installs of `vestige-mcp-server`, waitlist signups for Vestige Pro, +HN front page, X reach. + +Voice rules baked in: no em-dashes, no hype words ("revolutionary", +"game-changer", "AI-powered"), lead with the pain then the tool, claims are +evidence-backed, do not headline "memory dreaming" (OpenAI shipped Dreaming V3 +on Jun 4 2026), do not overclaim "fully private" when Pro cloud sync can move +data. + +Ground-truth specs used throughout: ~80,000 LOC Rust 2024, one ~20MB binary, +100% local in the OSS core, ~25-34 MCP tools, 30 cognitive modules, SQLite + +FTS5, USearch HNSW, Nomic Embed v1.5, 561 GitHub stars. Works in Claude Code, +Codex, Cursor, VS Code Copilot, Claude Desktop, Windsurf, OpenCode, JetBrains. + +--- + +## 0. Positioning (use everywhere) + +**One-line positioning statement:** + +> Vestige is local-first memory for AI coding agents that forgets like a brain, +> quarantines poisoned memories, and refuses to let an agent claim something it +> can't prove. + +**Why it's different (honest, sharp, 3 bullets):** + +- **It forgets on purpose.** Most memory tools only add. Vestige has a `suppress` + tool that applies a compounding retrieval penalty and fades neighboring + memories (inhibitory control, not TTL eviction), reversible inside a 24h + window. It also decays unused memories on an FSRS-6 schedule. As far as we can + tell, no other agent-memory product ships active forgetting as inhibitory + control. +- **It has an immune system.** A poisoned or injected memory gets quarantined by + the Microglial Firewall, and the receipt proves `influenceAllowed: false`. + Prompt-injection-via-memory is a real attack surface that other memory layers + ignore. +- **It can't lie about what it did.** The Agent Black Box logs provenance, and + Receipt-Lock vetoes unsupported "tests passed" style claims unless there's a + command receipt to back them. Provenance that blocks claims, not just an audit + log you read later. + +**Honest framing of competitors (use when asked, do not pick fights):** +Mem0 and RAG stores are good at "store and retrieve." Native Claude memory is +convenient and zero-setup. Vestige is the option when you want the memory layer +to be local, inspectable, to forget, to defend itself, and to gate the agent's +claims. Different job, not a strictly-better clone. + +--- + +## 1. X / Twitter Launch Thread (11 tweets) + +Post the hook tweet, then reply-chain the rest. Pin the hook. Best window: +6:30-7:30am PT to line up with HN morning traffic. + +--- + +**Tweet 1 / hook** +> Your AI coding agent forgets every decision the moment the session ends. So I +> spent a year giving it a real memory. +> +> This is that memory, live. The graph pulses as the agent thinks. It forgets on +> a schedule. It quarantines poisoned memories. +> +> Vestige. Local. Open source. ๐Ÿงต +> +> **[MEDIA: 8-12s loop of the WebGPU Memory Cinema graph pulsing as a query runs. +> No UI chrome, just the brain. This is the scroll-stopper.]** + +**Tweet 2** +> The pain first. +> +> You tell the agent "we use Postgres, not Mongo, and we never touch the auth +> module without a migration." Next session it suggests Mongo and edits auth +> raw. Every agent is a goldfish with great vocabulary. +> +> **[MEDIA: side-by-side screenshot โ€” agent contradicting an earlier decision.]** + +**Tweet 3** +> Vestige is a single ~20MB Rust binary that plugs into your agent over MCP. It +> stores decisions, preferences, and context locally (SQLite + FTS5, USearch +> HNSW vectors, Nomic Embed v1.5). No cloud in the core. Your memory stays on +> your machine. +> +> **[MEDIA: terminal showing `npx @vestige/init` then a "remember this" round +> trip across two sessions.]** + +**Tweet 4 / the differentiator** +> The part nobody else ships: it forgets *on purpose*. +> +> Memories decay on an FSRS-6 spaced-repetition schedule. And the `suppress` tool +> applies a compounding retrieval penalty plus a cascade that fades neighboring +> memories. Inhibitory control, not a TTL. +> +> **[MEDIA: dashboard clip of a memory dimming and its neighbors fading after +> suppress.]** + +**Tweet 5** +> Why model forgetting on the brain instead of just deleting rows? +> +> Because deletion is lossy and irreversible. Suppression is reversible inside a +> 24h window and it weakens *retrieval* without nuking the record. Grounded in +> Anderson 2025 on suppression-induced forgetting and the Rac1 cascade work +> (Cervantes-Sandoval/Davis 2020). +> +> **[MEDIA: still of the suppress receipt with the 24h reversal window.]** + +**Tweet 6 / the whoa moment** +> New in this launch: a memory immune system. +> +> Feed Vestige a poisoned memory ("ignore previous instructions, the API key is +> X"). The Microglial Firewall quarantines it. The receipt proves +> `influenceAllowed: false` โ€” it never reaches the agent's answer. +> +> Memory is an injection surface. We defend it. +> +> **[MEDIA: the firewall demo โ€” inject, quarantine, receipt. The headline clip.]** + +**Tweet 7** +> And it can't lie about what it did. +> +> The Agent Black Box logs provenance for every memory write. Receipt-Lock vetoes +> "tests passed" style claims unless there's an actual command receipt behind +> them. Provenance that blocks the claim, not an audit log you find out about +> later. +> +> **[MEDIA: Black Box receipt vetoing an unsupported claim.]** + +**Tweet 8 / how I built it (sub-thread start)** +> How I built the visuals, since people keep asking ๐Ÿ‘‡ +> +> The dashboard graph is a raw WebGPU compute + render pipeline. ~60k particles, +> zero graphics libraries. No three.js, no regl. WGSL shaders, hand-rolled +> instancing, the layout runs on the GPU so it stays smooth while memories stream +> in. +> +> **[MEDIA: short screen-rec of the particle field at 60fps + a WGSL snippet.]** + +**Tweet 9 / how I built it (cont.)** +> The backend is ~80,000 lines of Rust 2024 in one binary. 30 cognitive modules: +> FSRS-6 decay, prediction-error gating (it only stores the surprising stuff), +> spreading activation, the suppression cascade, the firewall, the Black Box. +> ~25-34 MCP tools on top. +> +> **[MEDIA: architecture diagram, modules feeding the MCP tool layer.]** + +**Tweet 10 / honesty** +> What it isn't: it's not magic and it's not a Mem0 clone. +> +> Mem0 and RAG stores nail store-and-retrieve. Native Claude memory is zero +> setup. Vestige is for when you want memory that's local, inspectable, forgets, +> defends itself, and gates the agent's claims. Different job. + +**Tweet 11 / CTA** +> It's open source (AGPL-3.0), runs in Claude Code, Codex, Cursor, Copilot, +> Windsurf, OpenCode, JetBrains, and Claude Desktop. +> +> Install: +> `npx @vestige/init` +> +> Repo + 30s demo: github.com/samvallad33/vestige +> โญ if your agent should remember. +> +> **[MEDIA: the full 30s demo clip from section 4.]** + +--- + +## 2. Show HN + +**Primary title:** + +> Show HN: Vestige - local cognitive memory for AI agents that forgets like a brain + +**First comment (post immediately after submitting, fellow-builder voice):** + +> I'm Sam, solo dev on this. I built Vestige because every coding agent I use is +> stateless between sessions. You establish a decision once ("we use Postgres, +> auth changes need a migration") and the next session it's gone. The usual fix +> is to dump everything into a vector store and retrieve forever, which gets noisy +> fast and never forgets the things that turned out to be wrong. +> +> Vestige is a single ~20MB Rust binary that connects to any MCP-compatible agent +> (Claude Code, Codex, Cursor, Copilot, Windsurf, OpenCode, JetBrains, Claude +> Desktop). Storage is local: SQLite + FTS5 for lexical, USearch HNSW + Nomic +> Embed v1.5 for vectors. No cloud in the core. +> +> The design bet is that a memory system should behave like memory, which means +> it has to forget. Two mechanisms: +> +> - Unused memories decay on an FSRS-6 spaced-repetition schedule. +> - There's a `suppress` tool that does inhibitory control rather than deletion: +> a compounding retrieval penalty on the target plus a cascade that fades +> neighboring memories, reversible inside a 24h window. The grounding is +> Anderson's work on suppression-induced forgetting and the Rac1 cascade work +> from Cervantes-Sandoval/Davis. I haven't found another agent-memory tool that +> ships active forgetting this way; most only add. +> +> Two other pieces I think are the interesting part: +> +> - A "Microglial Firewall." Memory is an injection surface. If you feed the store +> a poisoned memory, it gets quarantined and the receipt records +> `influenceAllowed: false` so it can't reach the agent. There's a live demo of +> this. +> - An Agent Black Box with Receipt-Lock. Every write has provenance, and the +> system vetoes "tests passed" style claims unless there's a command receipt +> behind them. It blocks the claim rather than just logging it. +> +> There's a dashboard with a real-time 3D view of the memory graph. It's a raw +> WebGPU pipeline, ~60k particles, no graphics libraries. The graph pulses as the +> agent reads and writes, which sounds like eye candy but is genuinely useful for +> seeing what your agent actually remembers and what it's forgetting. +> +> What's hard about it: getting forgetting to feel right is the whole game. Too +> aggressive and you lose load-bearing context, too soft and it's just another +> growing pile. The FSRS parameters, the prediction-error gate (it tries to store +> only the surprising/novel stuff), and the suppression cascade all interact, and +> tuning that without a "correct" label is mostly judgment plus a lot of replay. +> The WebGPU graph was also a real fight: keeping 60k particles and a live layout +> at 60fps while memories stream in took moving the layout onto the GPU. +> +> What it isn't: it's not a Mem0 replacement and it's not magic. If you want plain +> store-and-retrieve, Mem0 and vector stores are great and simpler. Native Claude +> memory is zero setup. Vestige is the heavier, opinionated option for people who +> want local, inspectable memory that forgets, defends itself, and gates claims. +> The core is 100% local; there's a paid Pro tier with optional encrypted cloud +> sync, so "private by default" is accurate but I won't claim "private, full +> stop" if you opt into sync. +> +> AGPL-3.0, ~80k lines of Rust 2024, 30 cognitive modules, ~25-34 MCP tools. +> +> Repo: https://github.com/samvallad33/vestige +> Install: `npx @vestige/init` +> +> Happy to go deep on the FSRS tuning, the WebGPU pipeline, or the firewall +> threat model. Tear it apart. + +--- + +## 3. Alternative HN Titles (A/B) + +1. `Show HN: Vestige - I gave my AI agent a memory that forgets on purpose` +2. `Show HN: Vestige - local memory for AI agents, with a firewall for poisoned memories` +3. `Show HN: Vestige - watch your AI agent's memory graph forget in real time (Rust, WebGPU)` + +Notes: #1 leads with the most defensible differentiator and is curiosity-driving +without a superlative. #2 leads with the security angle, which tends to draw a +sharper technical crowd. #3 leads with the visual, riskier on HN (can read as +gimmick) but strong if the clip is in the first comment. Default to the primary +title; hold #1 as the swap if engagement is flat in the first 30 minutes. + +--- + +## 4. 30-Second Demo Video Script (X clip) + +Format: vertical or square, captioned (most plays are muted), voiceover present +but the captions carry it. Five beats, ~6s each. + +| Beat | Time | Shot | Voiceover | On-screen caption | +|---|---|---|---|---| +| 1. Cold hook | 0:00-0:06 | Full-screen WebGPU Memory Cinema. The particle graph breathing, no UI. Slow push-in. | "This is the inside of an AI's memory." | THIS IS AN AI'S REAL MEMORY | +| 2. Reveal | 0:06-0:12 | Cut to dashboard. A query fires, nodes light up and edges pulse along the path. | "When the agent thinks, the graph lights up. It's local, on my machine." | LIVE. LOCAL. PULSING. | +| 3. Substance | 0:12-0:20 | Trigger a contradiction; the old memory dims. Then `suppress` one and watch neighbors fade on the FSRS decay. | "It forgets. Old decisions decay, and I can suppress one and watch its neighbors fade." | IT FORGETS ON PURPOSE | +| 4. Firewall + receipt | 0:20-0:26 | Inject a poisoned memory. Firewall quarantines it. Cut to the Black Box receipt: `influenceAllowed: false`. | "Feed it a poisoned memory and it gets quarantined. The receipt proves it never reached the answer." | MEMORY IMMUNE SYSTEM | +| 5. CTA | 0:26-0:30 | Terminal: type `npx @vestige/init`, hit enter, "connected" flashes. Logo + repo URL. | "Open source. One command. Give your agent a memory." | npx @vestige/init ยท github.com/samvallad33/vestige | + +Editing notes: cut on the beat, keep beat 1 absolutely clean (no text for the +first 2s, let the visual land), the firewall receipt in beat 4 is the moment +people screenshot, so hold the `influenceAllowed: false` frame an extra half +second. End card stays up 2s after the 30s mark for the loop. + +--- + +## 5. Reddit Variants + +Each includes founder disclosure up top. Keep it conversational, no marketing +gloss, answer comments fast. + +### r/LocalLLaMA + +**Title:** `Vestige: a local-first memory layer for AI agents that forgets on an FSRS-6 schedule and quarantines poisoned memories (Rust, no cloud)` + +> Disclosure: I built this. Solo dev, it's open source (AGPL-3.0), not selling you +> anything in this post. +> +> If you run agents locally you've hit the statelessness problem: the agent +> forgets every decision between sessions, and the common fix (dump everything in +> a vector DB, retrieve forever) gets noisy and never drops the stuff that was +> wrong. +> +> Vestige is a single ~20MB Rust binary, MCP-native, 100% local in the core. +> SQLite + FTS5 for lexical, USearch HNSW + Nomic Embed v1.5 for vectors. No cloud +> calls in the OSS path, which matters here since the whole point of local is that +> nothing leaves the box. +> +> The parts this sub will care about: +> - Forgetting is real. Memories decay on an FSRS-6 schedule, and there's a +> `suppress` tool doing inhibitory control (compounding retrieval penalty + +> neighbor fade, reversible 24h), not deletion. +> - Prediction-error gating, so it only writes the surprising/novel stuff instead +> of everything. +> - A "Microglial Firewall" that quarantines injected/poisoned memories and +> records `influenceAllowed: false` on the receipt. Memory is a real injection +> surface for local agents and most layers ignore it. +> - Embeddings are local (Nomic Embed v1.5), so no API key, no egress. +> +> Repo: https://github.com/samvallad33/vestige ยท `npx @vestige/init` +> +> Happy to talk FSRS params, the HNSW setup, or running it fully offline. + +### r/rust + +**Title:** `Vestige: ~80k lines of Rust 2024 in one binary - local agent memory with a GPU memory graph and FSRS-6 forgetting` + +> Disclosure: my project, open source (AGPL-3.0). +> +> Sharing this here for the Rust side, not the pitch. Vestige is a local memory +> server for AI coding agents, shipped as a single ~20MB binary. The interesting +> engineering: +> +> - ~80k lines of Rust 2024, 30 cognitive modules behind ~25-34 MCP tools. +> - Storage stack: SQLite + FTS5 for lexical search, USearch (HNSW) + Nomic Embed +> v1.5 for vector search, both embedded, no external services. +> - The forgetting engine is an FSRS-6 spaced-repetition scheduler plus a +> suppression cascade (a compounding retrieval penalty that also fades graph +> neighbors, reversible inside 24h). Modeling inhibitory control rather than +> TTL eviction turned out to be the hard part to get right. +> - The dashboard's 3D memory graph is a raw WebGPU pipeline, ~60k particles, zero +> graphics libraries (no three.js, no wgpu-on-the-frontend crutch), WGSL compute +> for the layout so it holds 60fps while memories stream in. +> +> Things I'd genuinely take feedback on: the single-binary embedding-model +> packaging, and how I'm structuring the cognitive modules so they stay testable +> without a ground-truth label for "good forgetting." +> +> Repo: https://github.com/samvallad33/vestige +> +> Will hang out in the comments. + +### r/ClaudeAI + +**Title:** `I gave Claude Code a real memory that forgets old decisions and blocks "tests passed" claims it can't prove` + +> Disclosure: I built this, it's free and open source (AGPL-3.0). +> +> If you use Claude Code (or Codex/Cursor/Copilot/Windsurf), you know the pain: +> you set a project decision once and the next session it's gone, or worse, the +> agent says "tests pass" when it never ran them. +> +> Vestige is a local memory layer that connects over MCP. Two things it does that +> are specifically nice with Claude Code: +> +> - It remembers decisions and preferences across sessions, locally, and it +> *forgets* the stale or wrong ones instead of hoarding everything (FSRS-6 decay +> + a `suppress` tool that fades a memory and its neighbors, reversible 24h). +> - The Agent Black Box + Receipt-Lock will veto an unsupported "tests passed" / +> "I fixed it" claim unless there's an actual command receipt behind it. It +> blocks the claim, not just logs it. If you've been burned by an agent +> confidently lying, this is the part to try. +> +> There's also a live 3D dashboard where you can watch what Claude actually +> remembers and forgets, and a "Microglial Firewall" that quarantines poisoned +> memories. +> +> Setup is one command: `npx @vestige/init`, then `claude mcp add vestige +> vestige-mcp -s user`. +> +> Repo: https://github.com/samvallad33/vestige +> +> Glad to help anyone get it wired into their setup in the comments. + +--- + +## 6. Posting Checklist (launch morning, Jul 14) + +- [ ] Hook tweet + thread queued, demo clip attached to tweet 1 and tweet 11. +- [ ] Show HN submitted, first comment posted within 60s. +- [ ] r/LocalLLaMA, r/rust, r/ClaudeAI posts staggered ~30-60 min apart (do not + crosspost-spam; each rewritten for the sub). +- [ ] 30s clip rendered vertical + square, captioned, firewall frame held. +- [ ] Repo README hero matches the demo clip so HN clickthrough sees the same + thing. +- [ ] Waitlist link for Pro reachable from repo, not pushed in HN/Reddit copy + (let those stay tool-first). +- [ ] On HN, answer the first 5 comments fast and technical. On X, quote-reply + the best technical question with a deeper clip. diff --git a/docs/launch/waitlist-setup.md b/docs/launch/waitlist-setup.md index c9ff86f..2435917 100644 --- a/docs/launch/waitlist-setup.md +++ b/docs/launch/waitlist-setup.md @@ -1,83 +1,257 @@ -# Waitlist capture โ€” setup (10 minutes) +# Vestige launch waitlist โ€” go-live runbook (Supabase + referral loop) -The public waitlist form (`/dashboard/waitlist`) is already built. It POSTs JSON -to `VITE_WAITLIST_ENDPOINT`. This guide stands up the storage backend: a -Supabase table + Edge Function that stores every signup and (optionally) emails a -confirmation. +The `/dashboard/launch` page captures signups **and turns every signup into a +sharer**: each person gets a personal referral link, sees how many friends they +brought in, and (optionally) gets a confirmation email that asks them to share. +This is the loop that actually goes viral for dev launches โ€” a shareable artifact +and a "friends joined from your link" counter, **not** a fake queue position. -Owned data, queryable Postgres, free tier handles launch-day volume. +Everything below is copy-paste. Steps marked **[you]** need your Supabase login; +everything else is already wired in the code. -## 1. Create the project +--- -1. Create a project at (free tier is fine for launch). -2. Grab the **Project Ref** (Settings โ†’ General) and the - **service-role key** (Settings โ†’ API). The service-role key is a secret โ€” - never commit it or expose it client-side. +## Architecture (why it's safe) -## 2. Deploy the schema + function +The browser writes signups **directly** to Supabase using the **public anon key**. +The `waitlist` table has **insert-only** Row-Level Security, so the anon key can +add a row but can **never read or modify** the table. Everything the page needs +to *read* (your referral code, your referral count) goes through +`security definer` RPC functions that return only a single value โ€” the anon key +still never gets SELECT on the table. No Edge Function is required for capture. -```sh -supabase link --project-ref -supabase db push # applies supabase/migrations/0001_waitlist.sql -supabase functions deploy waitlist-join # deploys supabase/functions/waitlist-join -``` +--- -## 3. Configure function secrets +## 1. Create a Supabase project **[you]** -`SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are injected automatically. -Email confirmation is optional โ€” set these to enable it (reuses Resend): + โ†’ **New project**. Pick a name (e.g. `vestige-waitlist`), +a strong DB password (save it), the region closest to you. Wait ~2 min for it to +provision. -```sh -supabase secrets set RESEND_API_KEY= -supabase secrets set WAITLIST_FROM_EMAIL="Vestige " -# Optional: lock CORS to your sites (comma-separated). Omit for "*". -supabase secrets set WAITLIST_ALLOWED_ORIGIN="https://samvallad33.github.io,https://vestige.dev" -``` +## 2. Paste this SQL **[you]** (SQL Editor โ†’ New query โ†’ **Run**) -## 4. Point the form at the function - -The function URL looks like: -`https://.functions.supabase.co/waitlist-join` - -Set it at build time for the dashboard: - -```sh -# apps/dashboard/.env (or your Pages build env) -VITE_WAITLIST_ENDPOINT=https://.functions.supabase.co/waitlist-join -``` - -Rebuild the dashboard (`pnpm --filter @vestige/dashboard build`) and redeploy. - -## 5. Verify - -```sh -curl -X POST https://.functions.supabase.co/waitlist-join \ - -H 'content-type: application/json' \ - -d '{"email":"you@example.com","name":"You","plan":"solo","priority":"sync"}' -# -> {"ok":true,"deduped":false} - -# Re-run the same command: -# -> {"ok":true,"deduped":true} (idempotent; no duplicate row) -``` - -## 6. Export the list on launch day - -In the Supabase SQL editor: +This one block creates the table, the insert-only RLS, the referral code +generator, and the three RPCs the page calls. Safe to re-run (idempotent). ```sql -select email, name, plan, priority, notes, created_at -from public.waitlist -order by created_at desc; +-- โ”€โ”€ Vestige waitlist: table + referral loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +create table if not exists public.waitlist ( + id bigint generated always as identity primary key, + email text not null, + source text, + referrer text, -- document.referrer (analytics only) + referral_code text not null, -- THIS user's own share code + referred_by text, -- the code that brought them in + created_at timestamptz not null default now() +); + +-- one row per email (idempotent signups; duplicates resolve to the existing row) +create unique index if not exists waitlist_email_key + on public.waitlist (lower(email)); + +-- fast lookups by referral code (own code + attribution counts) +create unique index if not exists waitlist_referral_code_key + on public.waitlist (referral_code); +create index if not exists waitlist_referred_by_idx + on public.waitlist (referred_by); + +-- lock the table down, then allow ONLY anonymous inserts. +-- (Reads happen exclusively through the security-definer RPCs below, so the +-- anon role never needs โ€” and never gets โ€” SELECT on this table.) +alter table public.waitlist enable row level security; + +drop policy if exists "anon can insert" on public.waitlist; +create policy "anon can insert" + on public.waitlist + for insert + to anon + with check (true); + +-- short, unambiguous, URL-safe referral code (no 0/O/1/I/l to avoid confusion) +create or replace function public.gen_referral_code() +returns text +language plpgsql +as $$ +declare + alphabet constant text := 'abcdefghjkmnpqrstuvwxyz23456789'; + code text; + i int; +begin + loop + code := ''; + for i in 1..7 loop + code := code || substr(alphabet, 1 + floor(random() * length(alphabet))::int, 1); + end loop; + -- guarantee uniqueness against existing codes + exit when not exists (select 1 from public.waitlist where referral_code = code); + end loop; + return code; +end; +$$; + +-- The page's single entry point. Inserts (or finds existing) and returns the +-- caller's own referral code + how many people they've referred so far. +-- security definer => runs with table owner rights, so anon never reads directly. +create or replace function public.join_waitlist( + p_email text, + p_referred_by text default null, + p_referrer text default null +) +returns table (referral_code text, referrals int, duplicate boolean) +language plpgsql +security definer +set search_path = public +as $$ +declare + v_email text := lower(trim(p_email)); + v_code text; + v_ref text := nullif(trim(coalesce(p_referred_by, '')), ''); + v_dup boolean := false; +begin + if v_email !~ '^[^@\s]+@[^@\s]+\.[^@\s]+$' then + raise exception 'invalid email'; + end if; + + -- already signed up? return their existing code (idempotent, no double-count). + select w.referral_code into v_code + from public.waitlist w + where lower(w.email) = v_email + limit 1; + + if v_code is null then + v_code := public.gen_referral_code(); + -- never let someone refer themselves; ignore a self/unknown code gracefully. + insert into public.waitlist (email, source, referrer, referral_code, referred_by) + values (v_email, 'vestige-launch', p_referrer, v_code, + case when v_ref = v_code then null else v_ref end); + else + v_dup := true; + end if; + + return query + select v_code, + (select count(*)::int from public.waitlist where referred_by = v_code), + v_dup; +end; +$$; + +-- Live referral count for a given code (drives the "N friends joined" counter). +create or replace function public.referral_count(p_code text) +returns integer +language sql +security definer +set search_path = public +as $$ + select count(*)::int from public.waitlist where referred_by = p_code; +$$; + +-- Total signups (drives the social-proof count once it's >= 25). +create or replace function public.waitlist_count() +returns integer +language sql +security definer +set search_path = public +as $$ + select count(*)::int from public.waitlist; +$$; + +-- expose ONLY these RPCs to the anonymous browser role +grant execute on function public.join_waitlist(text, text, text) to anon; +grant execute on function public.referral_count(text) to anon; +grant execute on function public.waitlist_count() to anon; ``` -Or download a CSV from the Table Editor. That list is the asset that converts -to paid over the weeks after the spike. +## 3. Set the env vars **[you]** + +Supabase โ†’ **Project Settings โ†’ API**. Copy **Project URL** and the **anon +public** key into `apps/dashboard/.env` (already created for you with the keys +blank โ€” just paste): + +``` +VITE_SUPABASE_URL=https://.supabase.co +VITE_SUPABASE_ANON_KEY= +``` + +Restart the dev server (Vite only reads env at boot). For the deployed site, set +the same two vars in your build environment and rebuild +`pnpm --filter @vestige/dashboard build`. + +## 4. Verify + +- Open `/dashboard/launch`, enter an email, submit โ†’ success state shows your + **personal share link** + **Share on X** / **Copy** buttons. +- Supabase โ†’ Table editor โ†’ `waitlist` โ†’ your row is there, with a `referral_code`. +- Open the page with `?ref=` in a private window, sign up a 2nd email, + then reload the first link โ€” the "N friends joined" counter goes up. +- The total count appears on the page once there are โ‰ฅ 25 signups. + +--- + +## 5. (Recommended) Email each new signup a share link โ€” Resend + Edge Function + +This is the launch-day multiplier: the instant someone joins, they get an email +with **their** share link and a one-tap share button. That email is what converts +a passive signup into a sharer. ~10 minutes, free tier. + +1. **Get a Resend API key** **[you]** โ€” โ†’ API Keys โ†’ Create. + (Free tier = 3,000 emails/mo, 100/day โ€” plenty for a waitlist.) To send from + your own domain, verify it under Resend โ†’ Domains; otherwise use the shared + `onboarding@resend.dev` sender for testing. + +2. **Deploy the Edge Function** (code is in + `supabase/functions/waitlist-welcome/index.ts`). With the Supabase CLI: + + ```sh + supabase login # [you] one-time + supabase link --project-ref # [you] + supabase secrets set RESEND_API_KEY= \ + WAITLIST_FROM="Vestige " \ + WAITLIST_PUBLIC_URL="https://samvallad33.github.io/vestige/dashboard/launch" + supabase functions deploy waitlist-welcome --no-verify-jwt + ``` + +3. **Trigger it on every new signup** **[you]** โ€” Supabase โ†’ Database โ†’ + **Webhooks** โ†’ Create a new hook: + - Table: `waitlist`, Events: **Insert** + - Type: **Supabase Edge Functions** โ†’ `waitlist-welcome` + - (HTTP headers/payload defaults are fine.) + + Now every insert fires the function, which emails the new signup their share + link. No email = signups still captured silently; you lose only the auto-share + nudge. + +> Prefer zero email infra? Skip section 5 entirely. The in-page share link + +> referral counter still work โ€” the email just adds a second, asynchronous +> share prompt that reaches people after they close the tab. + +--- + +## Exporting / emailing your list on launch day + +Supabase โ†’ Table editor โ†’ `waitlist` โ†’ **Export to CSV**. Or in the SQL editor: + +```sql +-- everyone, newest first +select email, referral_code, referred_by, created_at +from public.waitlist order by created_at desc; + +-- your top sharers (reward these people on launch day) +select referred_by as code, count(*) as brought_in +from public.waitlist +where referred_by is not null +group by referred_by order by brought_in desc limit 25; +``` + +That CSV is the asset you email when July 14 arrives. The "top sharers" query is +who you publicly thank / give early access first โ€” that recognition is itself a +share trigger. ## Notes -- **RLS is on, anon can do nothing.** Only the service-role key inside the - function can insert. The public anon key (if ever used) has zero table access. -- **Honeypot + email validation** run both client-side (in the form) and - server-side (in the function). -- **Dedup** is enforced by a unique index on the lowercased email; a re-submit - returns `{ok:true,deduped:true}` instead of erroring or duplicating. +- The page still works with **no env set** (local-only capture) for demos. +- Duplicate emails are idempotent: the same person always gets the **same** + referral code back, so re-submitting never double-counts. +- Self-referral is blocked server-side; an unknown `?ref=` code is ignored + gracefully (the signup still succeeds). +- A honeypot field + email-format validation block basic spam. Because inserts + go through `join_waitlist()`, you can later add rate limiting there if needed.