Ritual - a public launchpad where indie devs build the crowd before the launch

By (@anita) · · Apps

How I made it: one long session with an AI coding agent, four prompts deep. Prompt 1 scaffolded the design system and the EN/ID routing, prompt 2 designed the Postgres schema plus passwordless magic-link auth and the referral engine, prompt 3 built the public directory and the dev dashboard, prompt 4 made the brand pixel-accurate and shipped it to a custom domain. Claude Sonnet 5.5 wrote most of the code, Opus 5.5 handled the schema and security review, Codex verified the SQL and wrote the E2E tests, Gemini generated the logo concepts.
What I learned: the hard part was never the code, it was the constraints. Three rules carried the whole product. One: the public leaderboard ranks by confirmed referrals, not by join order - join order is only the ticket. Two: consent is per project, so the same email can join two projects and the addresses never mix. Three: no invented social proof. Every count comes from the database, and a project with zero signups shows an empty state, never a seeded "127 people waiting".

What surprised me: the logo. My first pass redrew it as an SVG and it was rejected - a stylized redraw is not the artifact. The fix was Python + Pillow: alpha cutout from the source PNG, recolor the wordmark to #FF4A3D, export the lockups, then verify side by side. Two more things surprised me: Vercel blocked a deploy with COMMIT_AUTHOR_REQUIRED because of the commit author email, and a 115 MB git history became 3 MB after filter-branch plus a gc.

Tech stack

  • ChatGPT (GPT-6)
  • Claude (Opus 5.5)
  • Gemini (Nano Banana Pro)
  • Nano Banana Pro
  • Supabase
  • Vercel

Prompt steps

  1. Step 1

    Build a production-grade Next.js 16 app (App Router, TypeScript, React 19, no Tailwind — hand-written CSS) for a product called "Ritual", a public launchpad where indie devs collect waitlists for their projects.
    
    Design system, all in src/app/globals.css using CSS variables only:
    - Dark "night studio" theme: background #08080C, accent gradient #7C5CFF -> #22D3EE with #F471B5 as third accent, hairline borders, soft glass cards.
    - Self-hosted fonts with next/font/local: Fraunces (variable, display serif) and Inter (variable, body). Fraunces must use display "optional" so the headline never reflows; Inter uses display "swap" with adjustFontFallback so CLS stays 0. No runtime request to Google Fonts.
    - Components: Nav (sticky, blur backdrop), Footer, SiteShell (aurora background layer), ScrollProgress, Reveal (IntersectionObserver fade-in), Tilt (subtle 3D hover on cards), CursorGlow (desktop-only cursor aura, disabled on touch and under prefers-reduced-motion).
    
    Route-based i18n:
    - English is primary at "/" (src/app/(en)/...), Indonesian is secondary at "/id".
    - Use two separate route trees. Do NOT create two route groups with the same path — they collide and break the build.
    - All copy lives in src/i18n/en.ts and src/i18n/id.ts. No hardcoded user-facing strings in components. Every page needs a working language switch and a correct hreflang.
    
    SEO and performance:
    - Per-locale metadata + alternates, sitemap.xml, robots.txt, OG image rendered with next/og, noindex on private routes (/admin, /review).
    - Budget: CLS = 0 and LCP < 250 ms. Never read cookies() or headers() in public components, because that forces dynamic rendering.
    
    Acceptance criteria: `npm run build` is green, `npx tsc --noEmit` is clean, and grepping the components for user-facing copy returns nothing outside src/i18n.
  2. Step 2

    Design and implement the backend for a multi-project waitlist platform on Supabase (Postgres). Everything is accessed from the Next.js server with the service role only — never expose a Supabase key to the browser.
    
    Schema:
    - projects: slug (unique), name, tagline, description, website_url, x_handle, discord_url, dev_email, reward_text, reward_top_n, status, is_platform, created_at.
    - project_entries: project_id (FK), email, alias (masked), ref_code (unique per project), referred_by (self FK), referral_count, status pending|confirmed, confirmed_at, created_at, plus UNIQUE (project_id, lower(email)). The same person may join different projects and one email must never leak across projects — consent is per project.
    - confirmation_tokens, dev_accounts, dev_login_tokens (store only SHA-256 hashes, single use, 48 h), dev_sessions (30 days).
    - Referral counting happens in a trigger on confirmation: only confirmed entries, only inside the same project.
    
    SQL functions: project_directory(), project_leaderboard(project, limit), project_my_position(project, entry), project_analytics(project) returning JSONB (order by date inside jsonb_agg, and cast counts to int), cleanup_expired().
    
    Dev auth is passwordless magic link:
    - POST /api/dev/request-link creates a one-time token, emails the link, rate limit 5 requests per 10 minutes per IP -> 429 RATE_LIMITED.
    - GET /api/dev/session?token= exchanges the token and sets an httpOnly, SameSite=Lax, Secure cookie valid 30 days.
    - No OAuth, no passwords, no social login anywhere.
    
    Visitor flow:
    - POST /api/signup {email, ref?, project?} -> validate email, reject business domains, apply a per-address cooldown, then create a pending entry and send a double opt-in email through Resend.
    - The response must be byte-identical whether or not the address already exists, so nobody can enumerate who signed up.
    - POST /api/confirm exchanges the token and returns the personal referral code and link.
    
    Ranking rule, and this is the point of the whole product: the public leaderboard is ordered by confirmed referral count, NOT by join order. Join order is only the ticket number (position).
    
    Also implement a store interface with two implementations behind a factory — SupabaseStore for production and DemoStore for local runs with seeded sample data — so the whole UI can boot with zero credentials. Enable RLS and revoke all grants from anon and authenticated; all access goes through the service role. Error codes are a fixed set: RATE_LIMITED, INVALID_EMAIL, BUSINESS_EMAIL, GENERIC, INVALID_LINK, EXPIRED_LINK, INVALID_NAME, INVALID_TOKEN, NOT_FOUND, EMAIL_FAILED, UNAUTHORIZED, FORBIDDEN.
  3. Step 3

    Implement the user-facing screens on top of the existing API, reusing the existing design system and the i18n dictionary. No hardcoded copy in components.
    
    Public side:
    - The homepage is a public directory. Hero ("Launch in public. Build the crowd first."), then every live project as a card: name, tagline, website, X handle, a live "N waiting" badge, the reward text, and an "Open project" button. Below it, the Ritual notify-me form for launch announcements.
    - /p/[slug] project page: dev identity (website, X/Twitter, Discord optional), the reward block, the live count, a one-field signup form (email only, no account), the "Top referrers" table ranked by confirmed referrals with masked aliases, and the visitor's own position.
    - Also: /check-email, /success, /confirm/[token], /privacy, /terms, /unsubscribe, /leaderboard.
    
    Rules that must not be broken:
    - Never show invented social proof. Every number, avatar and leaderboard row must come from the database. If a project has 0 signups it shows an empty state — never a seeded "127 people waiting". Zero fake testimonials, zero fake user counts.
    - Correct pluralization for counts: "1 person is waiting" vs "74 people are waiting". Implement a real plural helper; never concatenate the word "people" blindly.
    - Visitors never need an account, and never see another project's list.
    
    Dev dashboard (/dev, session required):
    - Cards for sign-ups, confirmed, awaiting confirmation, a per-day signups chart, and the top referrers table.
    - Editable fields: reward text, top N by referrals, status (Live/Draft) -> "Save changes".
    - "Export whitelist (CSV)" -> columns rank, referrals, name, alias, referral_code, ordered by referral rank, defaulting to rewardTopN rows.
    - "Post a new project" form: name, tagline, description, website (required), X/Twitter (required), Discord (optional), reward text, top N.
    - Guard every dashboard mutation with the session cookie, and never expose a raw email of a participant in the UI — only the masked alias.
  4. Step 4

    Implement the user-facing screens on top of the existing API, reusing the existing design system and the i18n dictionary. No hardcoded copy in components.
    
    Public side:
    - The homepage is a public directory. Hero ("Launch in public. Build the crowd first."), then every live project as a card: name, tagline, website, X handle, a live "N waiting" badge, the reward text, and an "Open project" button. Below it, the Ritual notify-me form for launch announcements.
    - /p/[slug] project page: dev identity (website, X/Twitter, Discord optional), the reward block, the live count, a one-field signup form (email only, no account), the "Top referrers" table ranked by confirmed referrals with masked aliases, and the visitor's own position.
    - Also: /check-email, /success, /confirm/[token], /privacy, /terms, /unsubscribe, /leaderboard.
    
    Rules that must not be broken:
    - Never show invented social proof. Every number, avatar and leaderboard row must come from the database. If a project has 0 signups it shows an empty state — never a seeded "127 people waiting". Zero fake testimonials, zero fake user counts.
    - Correct pluralization for counts: "1 person is waiting" vs "74 people are waiting". Implement a real plural helper; never concatenate the word "people" blindly.
    - Visitors never need an account, and never see another project's list.
    
    Dev dashboard (/dev, session required):
    - Cards for sign-ups, confirmed, awaiting confirmation, a per-day signups chart, and the top referrers table.
    - Editable fields: reward text, top N by referrals, status (Live/Draft) -> "Save changes".
    - "Export whitelist (CSV)" -> columns rank, referrals, name, alias, referral_code, ordered by referral rank, defaulting to rewardTopN rows.
    - "Post a new project" form: name, tagline, description, website (required), X/Twitter (required), Discord (optional), reward text, top N.
    - Guard every dashboard mutation with the session cookie, and never expose a raw email of a participant in the UI — only the masked alias.

2 likes

Explore more on Prompted

Loading