mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
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:
parent
6837198328
commit
a2f4e459e1
2 changed files with 642 additions and 66 deletions
402
docs/launch/VIRAL_ASSETS_2026-07-14.md
Normal file
402
docs/launch/VIRAL_ASSETS_2026-07-14.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue