Paid waitlist for Hina — build plan
5 minThe loop Stack Routes Checkout flow Supabase schema The notify automation Whitelist bridge The cold-open constraint Build steps Open decisions
A stranger watches Hina text → signs in → drops their phone → pays $99. Supabase holds the row, your assistant texts you the second it lands, you add them to the whitelist and send Hina's opener. Hina takes the thread from there.
| Piece | Choice | Why |
|---|---|---|
| Landing app | SvelteKit (Svelte 5) on Vercel | Server routes for Stripe + Supabase. Lives in language/web (sibling of agent/). |
| Auth + DB | Supabase | Authed state, a user record, and a dashboard — all three at once. Table editor = your view. |
| Payments | Stripe Checkout + webhook | Hosted page, no PCI surface. Webhook sets paid = true. |
| Notify | morpheus/assistant (Mac) polls Supabase | Texts you the instant a member pays. iMessage ⇒ must run on the Mac (see automation). |
| Delivery | Hina agent (Mac, existing) | You send the opener; Hina runs the thread. No changes beyond the whitelist. |
Note: this reverses the earlier "no Supabase" call — justified the moment you wanted authed state + a visible user record. Still no Twilio.
| Path | What's there |
|---|---|
/ | Landing. Full-bleed Hina demo video + the "text message" CTA button → takes you to /pricing. |
/pricing | Founder pricing only (for now) — $99/yr, Founding 10. Sign in (Google) → phone → Stripe Checkout. |
Order matters. Auth and phone come before the charge so Stripe is the last step (best completion) and you never take money from someone you can't reach.
checkout.session.completed the webhook flips paid = true + stamps paid_at.Why not Stripe-first / Google Form: losing a maybe-buyer at sign-in is a normal funnel; losing a paid user's phone is fatal — the product is texting that number. A form can't give authed state or tie a row to an identity.
One table. It is the CRM, the funnel view, and the automation's queue — all at once.
create table users (
id uuid primary key references auth.users,
email text,
phone text, -- normalized at write, matches iMessage handle
paid boolean default false,
paid_at timestamptz,
notified boolean default false, -- assistant flips this after it texts you
created_at timestamptz default now()
);
You want a text the second a paid member lands. The seam is deliberately decoupled: the landing page only writes the row — the consumer can change without touching the web app.
select * from users where paid = true and notified = false → text you each one → set notified = true. Exactly one ping per member, survives restarts.Later, if you ever want notify off your Mac, swap the poller for a Supabase webhook → push/Telegram. The row contract doesn't change.
Gotcha in today's code: the whitelist is frozen at boot — config.mjs reads ALLOW_FROM from .env and the poller builds the allow-set once at startup (chat-poller.mjs:300). Adding a number today means editing .env and restarting.
normalizeHandle() so the checkout phone matches the iMessage handle.npm run allow +1555… appends to the file — same moment you send the opener. No cloud sync.Hina can't cold-text a stranger. The agent only reacts to inbound iMessages and its proactive engine needs an existing chat to poke — it can't open a brand-new thread to a number that's never messaged it. So you send the first text by hand; Hina takes over from their reply. At 10 users this is ~30s of work and it preserves the "Hina texts you first" magic.
language/webRoutes / (video + CTA) and /pricing (founder pricing). Hero video placeholder.paid/paid_at.npm run allowIn language/. Small change to config + poller.morpheus/assistant. Polls Supabase, texts you, flips notified.paid = true rows; flip the page to "Sold out".language/web · SvelteKit (Svelte 5) · agent renamed Hina · routes / + /pricing.2026-06-24-pricing doc before building the pricing section.