A complete, remixable template for gating content behind a Stripe subscription on Val Town.
| Layer | How |
|---|---|
| Auth | Google Sign-In via LastLogin — zero API keys needed |
| Payments | Stripe Checkout (Apple Pay, Google Pay, all cards — automatic) |
| Billing Portal | Stripe Customer Portal for self-serve manage / cancel |
| Database | Val Town SQLite — email ↔ stripe_customer_id ↔ subscription_status |
| Webhooks | Handles checkout.session.completed, subscription.updated, subscription.deleted |
Visitor → Landing page → Sign In (Google) → Paywall → Stripe Checkout → Premium Dashboard
↕
Stripe Customer Portal
(manage / cancel)
YOUR_VAL_URL/api/webhook
checkout.session.completed, customer.subscription.updated, customer.subscription.deleted| Variable | Value |
|---|---|
STRIPE_SECRET_KEY | sk_test_... |
STRIPE_PRICE_ID | price_... |
STRIPE_WEBHOOK_SECRET | whsec_... |
main.tsx — HTTP handler + routing (entry point)
db.ts — SQLite database layer
stripe.ts — Stripe API helpers (checkout, portal, webhooks)
styles.ts — All CSS (design tokens, page-specific styles)
pages/
landing.tsx — Public landing page with pricing
dashboard.tsx — Premium content (subscribers) / paywall (non-subscribers)
setup.tsx — Setup instructions (shown when env vars are missing)
| Route | Method | Purpose |
|---|---|---|
/ | GET | Landing / Dashboard / Setup (context-dependent) |
/api/checkout | POST | Creates Stripe Checkout Session |
/api/portal | POST | Creates Stripe Customer Portal Session |
/api/webhook | POST | Receives Stripe webhook events |
/api/user | GET | Returns subscription status (JSON) |
/auth/login | GET | Google auth (LastLogin middleware) |
/auth/logout | GET | Logout (LastLogin middleware) |
Edit STRIPE_PRICE_ID env var. The $9/mo shown in the UI is just placeholder text in pages/dashboard.tsx and pages/landing.tsx.
Edit PremiumContent component in pages/dashboard.tsx.
Edit styles.ts — all CSS variables and page styles are in one place.
Store tier info in the plan_name column and gate content by tier instead of just active/inactive.
Stripe Checkout automatically shows these buttons when:
No extra code needed.