Skip to content

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