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) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-27 11:13:10 -05:00
parent 6837198328
commit a2f4e459e1
2 changed files with 642 additions and 66 deletions

View file

@ -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.

View file

@ -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 <https://supabase.com> (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 <your-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):
<https://supabase.com>**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=<your-resend-key>
supabase secrets set WAITLIST_FROM_EMAIL="Vestige <waitlist@yourdomain>"
# 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://<project-ref>.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://<project-ref>.functions.supabase.co/waitlist-join
```
Rebuild the dashboard (`pnpm --filter @vestige/dashboard build`) and redeploy.
## 5. Verify
```sh
curl -X POST https://<project-ref>.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://<project-ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<anon public 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=<that code>` 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]** — <https://resend.com> → 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 <project-ref> # [you]
supabase secrets set RESEND_API_KEY=<your key> \
WAITLIST_FROM="Vestige <onboarding@resend.dev>" \
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.