A fast, statically-generated waitlist landing page for Mal, an AI-native digital bank testing appetite for a lending product in Egypt.
Live URL: https://<your-deployment>.vercel.app (replace after deploy)
- Next.js (App Router, TypeScript) — page is statically generated (SSG) for fastest load and full crawlability.
- Tailwind CSS v4 — utility styling, no CSS-in-JS, no component kit.
- Supabase (Postgres) — signups written server-side with the service-role key.
- Zod — server-side (authoritative) validation, mirrored client-side for UX.
- Deployed on Vercel.
The page itself is a Server Component; only the signup form is a Client Component. No analytics SDKs, no heavy images (all UI is CSS/SVG), one self-hosted font family via next/font.
# 1. Clone
git clone <your-repo-url>
cd mal-waitlist
# 2. Install dependencies
npm install
# 3. Configure environment
cp .env.example .env.local
# then edit .env.local with your real values (see below)
# 4. Run locally
npm run dev
# open http://localhost:3000The page renders and builds without Supabase configured. Env vars are only required for the form write to succeed at runtime.
Copy .env.example → .env.local and fill in:
| Variable | Description |
|---|---|
NEXT_PUBLIC_SITE_URL |
Public base URL of the deployed site, no trailing slash (e.g. https://mal-waitlist.vercel.app). Used for canonical URL, Open Graph, sitemap.xml, and robots.txt. Defaults to http://localhost:3000 in dev. The only public var — safe to expose. |
SUPABASE_URL |
Supabase project URL, e.g. https://xxxx.supabase.co. Found in Project Settings → API → Project URL. Server-side only. |
SUPABASE_SERVICE_ROLE_KEY |
Supabase service-role secret. Bypasses Row Level Security, so it is server-side only — never prefix with NEXT_PUBLIC_. Found in Project Settings → API → service_role. |
The migration lives at supabase/migrations/0001_init.sql. It creates public.waitlist_signups:
| Column | Type | Notes |
|---|---|---|
id |
uuid |
PK, gen_random_uuid() |
full_name |
text |
required |
email |
text |
nullable |
phone |
text |
nullable |
referral_source |
text |
nullable |
market |
text |
required, default 'egypt' |
created_at |
timestamptz |
default now() |
Constraints & indexes:
contact_requiredcheck:email IS NOT NULL OR phone IS NOT NULL— at least one contact method.- Unique index on
(lower(email), market)whereemail is not null— case-insensitive, one email per market, still allows phone-only signups. - Index on
(market, created_at desc)— fast "newest signups per market" admin query.
Row Level Security is enabled on the table and no policies are added. This means the anon and authenticated roles have zero access. All writes go through the service-role key on the server (which bypasses RLS), so the table stays fully locked to the public. There is no client-side database access anywhere in this app.
Option A — Supabase SQL Editor (fastest):
- Open your project → SQL Editor → New query.
- Paste the contents of
supabase/migrations/0001_init.sql. - Click Run.
Option B — Supabase CLI:
# one-time
supabase link --project-ref <your-project-ref>
# apply migrations in supabase/migrations/
supabase db push- Push this repo to GitHub/GitLab/Bitbucket.
- In Vercel: New Project → import the repo. Framework preset auto-detects Next.js.
- Add the environment variables (Project Settings → Environment Variables) for the Production (and Preview) environments:
NEXT_PUBLIC_SITE_URL→ your production URLSUPABASE_URLSUPABASE_SERVICE_ROLE_KEY
- Deploy. Vercel runs
npm run buildand serves the static page. - After the first deploy, set
NEXT_PUBLIC_SITE_URLto the real domain and redeploy so canonical/OG/sitemap URLs are correct.
- Anti-spam: the form already ships a honeypot field; next add Cloudflare Turnstile (invisible) and rate limiting on the Server Action (e.g. Upstash Redis, per-IP) to stop scripted floods.
- Double opt-in: send a confirmation email/SMS with a verification link and a
confirmed_atcolumn, so the list is deliverable and consented. - Event tracking: add a privacy-light analytics beacon (Vercel Analytics or a single Plausible call) to measure form-view → submit conversion and referral-source performance.
- A/B headline test: serve two hero headlines via a cookie split and record the variant on the signup row to see which drives more completions.
- Admin view: a tiny authenticated
/adminreadingwaitlist_signups(newest-first, per market) instead of eyeballing rows in Supabase.