Self-Service Signup + Stripe Integration Design¶
Date: 2026-05-19 GitHub Issues: #13 (signup), #14 (Stripe), #15 (E2E test)
Goal¶
Replace the waitlist flow with real self-service signup and payment so a user can sign up, get an API key, create links/QR codes, and upgrade to Pro — all without manual intervention.
Decisions¶
- Auth: Supabase Auth (email + password, JWT sessions, email verification, password reset)
- Dashboard: New routes in the existing landing page SPA (no build tooling)
- Payments: Stripe Checkout + Customer Portal (hosted by Stripe)
- Emails: On-screen only for now; Supabase Auth handles its own verification/reset emails
Authentication Flow¶
Supabase Auth handles signup, login, password reset, and email verification. The SPA talks directly to Supabase's JS client for auth operations.
Signup:
1. User clicks "Get API key" → signup form (email + password)
2. SPA calls supabase.auth.signUp() → Supabase creates user, sends verification email
3. On success, SPA calls POST /api-keys with Authorization: Bearer <jwt> → backend creates a free-tier API key → returns key + webhook secret
4. Dashboard shows key with copy button
Login:
1. User clicks "Log in" → email + password form
2. SPA calls supabase.auth.signInWithPassword() → gets JWT
3. SPA redirects to dashboard, fetches keys/usage via API with JWT
Backend auth: Existing authenticate_request() only checks X-API-Key. Add a second auth path for dashboard endpoints: validate Authorization: Bearer <jwt> against Supabase, extract user ID. API endpoints (POST /shorten, etc.) continue using X-API-Key only.
Modified endpoints:
- POST /api-keys — accept JWT auth (currently master-key only), create key owned by the authenticated user
- GET /api-keys — return only keys owned by the JWT user (master key still sees all)
Stripe Integration¶
Payment flow:
1. Dashboard user with free tier clicks "Upgrade to Pro"
2. SPA calls POST /checkout (JWT auth) → backend creates Stripe Checkout session with user's email and API key ID in metadata → returns checkout URL
3. User completes payment on Stripe's hosted page
4. Stripe sends checkout.session.completed webhook → backend reads API key ID from metadata → updates tier to "pro", stores stripe_customer_id and stripe_subscription_id
5. User returns to dashboard, sees Pro tier active
Cancellation: customer.subscription.deleted webhook → set tier back to "free". Existing links keep working.
Billing management: POST /billing-portal (JWT auth) → Stripe Customer Portal URL. User manages card, cancels, views invoices on Stripe's hosted UI.
Schema change — add columns to api_keys:
- user_id (UUID, references Supabase auth.users)
- stripe_customer_id (text, nullable)
- stripe_subscription_id (text, nullable)
Environment variables:
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
- STRIPE_PRICE_ID
Security: Stripe webhook verifies Stripe-Signature header. No JWT on that endpoint.
Dashboard UI¶
New SPA routes:
- #login — email + password form
- #signup — email + password form (replaces waitlist)
- #dashboard — authenticated view
Dashboard sections: 1. API Key card — key prefix, copy full key (only at creation), copy webhook secret, tier badge 2. Usage card — monthly links created + webhook deliveries with progress bars 3. Plan card — current tier, "Upgrade to Pro" or "Manage billing" button
Auth state: SPA checks supabase.auth.getSession() on load. Logged in → dashboard. Not logged in → landing page. Nav shows "Dashboard" when logged in.
Key reveal: Raw key shown only at creation time (backend stores hash only). Lost keys → use rotate endpoint.
Supabase JS client: Loaded via CDN script tag.
What Changes, What Doesn't¶
Untouched:
- POST /shorten, GET /{short_id}, GET /{short_id}/qr — keep using X-API-Key
- Webhook delivery, HMAC signing, retry logic
- Rate limiting, usage tracking, quota enforcement
- MkDocs docs
Modified:
- main.py — JWT validation, modify POST/GET /api-keys for JWT auth, add /checkout, /billing-portal, /stripe-webhook
- landing/app.jsx — add routes, auth state
- landing/signup.jsx — rewrite from waitlist to real signup
- landing/components.jsx — nav updates for auth state
- landing/index.html — add Supabase JS CDN
- supabase_schema.sql — add columns to api_keys
New files:
- landing/dashboard.jsx — dashboard component
- landing/login.jsx — login form component
Dependencies:
- stripe Python package in requirements.txt