Root (Vercel project)
├── api/
│ ├── create-checkout-session.js — Stripe checkout (serverless)
│ ├── stripe-webhook.js — Stripe webhook handler (serverless)
│ └── subscription-status.js — Server-side subscription check (serverless)
├── website/ — Static frontend (served as root)
│ ├── index.html — Landing page
│ ├── auth.html — Auth page (login/signup/forgot/reset)
│ ├── auth-callback.html — OAuth & email verification callback
│ ├── app.html — Dashboard (protected, requires session)
│ ├── billing.html — Billing/subscription management
│ ├── try.html — Demo page (works without login)
│ ├── config.js — Client-side keys (Supabase + Stripe publishable)
│ ├── auth.js — Auth page state machine
│ ├── app.js — Dashboard logic, session guard, paywall
│ ├── billing.js — Billing page logic
│ ├── try-auth.js — Try page auth state (header UI, paywall)
│ └── ...
├── supabase/
│ └── migrations/
│ ├── 001_init.sql — DB tables + RLS policies + triggers
│ └── 002_subscription_plan_column.sql — Add plan column
├── package.json
├── vercel.json
└── SETUP.md — This file
| Secret | Location | Notes |
|---|---|---|
SUPABASE_URL |
Vercel ENV + config.js |
Public URL, safe for client |
SUPABASE_ANON_KEY |
config.js only |
Publishable key, safe for client |
SUPABASE_SERVICE_ROLE_KEY |
Vercel ENV only | NEVER expose on client |
STRIPE_PUBLISHABLE_KEY |
config.js only |
pk_test_... / pk_live_... |
STRIPE_SECRET_KEY |
Vercel ENV only | NEVER expose on client |
STRIPE_WEBHOOK_SECRET |
Vercel ENV only | whsec_... |
SITE_URL |
Vercel ENV | Your frontend URL |
- Go to supabase.com → create or select your project
- Go to Settings → API and copy:
- Project URL → this is your
SUPABASE_URL - anon / public key → this is your
SUPABASE_ANON_KEY - service_role key → this is your
SUPABASE_SERVICE_ROLE_KEY(keep secret!)
- Project URL → this is your
- Update
website/config.jswith the URL and anon key
Run both SQL migrations in order in your Supabase SQL Editor:
- Go to SQL Editor in Supabase Dashboard
- Paste and run
supabase/migrations/001_init.sql - Paste and run
supabase/migrations/002_subscription_plan_column.sql
This creates:
profiles— auto-populated on signup via triggersubscriptions— managed by Stripe webhooks (write via service_role only)projects— saved reports (future feature)uploads— upload history (future feature)
All tables have RLS enabled. Users can only SELECT their own subscription. Only service_role (used by serverless functions) can INSERT/UPDATE subscriptions.
-
Go to Authentication → URL Configuration:
- Site URL:
https://your-domain.vercel.app(orhttp://localhost:3000for dev) - Redirect URLs (add all):
https://your-domain.vercel.app/auth-callback.html https://your-domain.vercel.app/app.html https://your-domain.vercel.app/auth.html http://localhost:3000/auth-callback.html http://localhost:3000/app.html http://localhost:3000/auth.html
- Site URL:
-
Email Provider (should be enabled by default):
- Authentication → Providers → Email → Enabled ✓
- Optionally disable "Confirm email" for faster testing
-
(Optional) Google OAuth:
- Go to console.cloud.google.com
- Create OAuth 2.0 Client ID
- Add authorized redirect URI:
https://<YOUR_PROJECT_REF>.supabase.co/auth/v1/callback - In Supabase: Authentication → Providers → Google → Enable, paste Client ID & Secret
-
Create three products:
Product Price Billing Starter $19/month Recurring, monthly Growth $59/month Recurring, monthly Pro $99/month Recurring, monthly -
After creating each price, copy the Price ID (
price_xxx...)
var STRIPE_PRICES = {
starter_monthly: 'price_YOUR_STARTER_ID',
growth_monthly: 'price_YOUR_GROWTH_ID',
pro_monthly: 'price_YOUR_PRO_ID',
};Also set your Stripe publishable key:
var STRIPE_PUBLISHABLE_KEY = 'pk_test_YOUR_KEY_HERE';- Go to Stripe Dashboard → Developers → Webhooks
- Click "Add endpoint"
- Endpoint URL:
https://your-domain.vercel.app/api/stripe-webhook - Select events to listen for:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paid
- Click "Add endpoint"
- Copy the Signing secret (
whsec_...) — this is yourSTRIPE_WEBHOOK_SECRET
cd StockWise
npm install# Install Vercel CLI
npm i -g vercel
# Login
vercel login
# Deploy (first time — will prompt for project setup)
vercel
# For production
vercel --prodIn Vercel Dashboard → Project → Settings → Environment Variables, add:
| Variable | Value | Environment |
|---|---|---|
SUPABASE_URL |
https://xxxxx.supabase.co |
Production, Preview, Development |
SUPABASE_SERVICE_ROLE_KEY |
eyJhbGci... (service_role key) |
Production, Preview, Development |
STRIPE_SECRET_KEY |
sk_test_... or sk_live_... |
Production, Preview, Development |
STRIPE_WEBHOOK_SECRET |
whsec_... |
Production, Preview, Development |
SITE_URL |
https://your-domain.vercel.app |
Production |
SITE_URL |
http://localhost:3000 |
Development |
Or via CLI:
vercel env add SUPABASE_URL
vercel env add SUPABASE_SERVICE_ROLE_KEY
vercel env add STRIPE_SECRET_KEY
vercel env add STRIPE_WEBHOOK_SECRET
vercel env add SITE_URLAfter adding env vars, redeploy:
vercel --prod# Install dependencies
npm install
# Run with Vercel dev (serves website/ + api/)
npx vercel dev
# Or just serve the frontend (API won't work):
cd website && python -m http.server 3000For local Stripe webhook testing, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/stripe-webhook
# Copy the webhook signing secret from the output and set itLanding (index.html)
├── "Log in" → auth.html?tab=login
├── "Start free" → auth.html?tab=signup
└── "Dashboard" (if logged in) → app.html
Auth (auth.html)
├── Login → Supabase signInWithPassword → try.html
├── Signup → Supabase signUp → "Check your email" panel
├── Forgot → resetPasswordForEmail → "Check your email"
├── Reset → updateUser({ password }) → try.html
└── Google → signInWithOAuth → auth-callback.html → try.html
App/Dashboard (app.html) — PROTECTED
├── Checks session → if none, redirect to auth.html
├── Checks subscription (server-side /api/subscription-status)
├── Active sub → full access
├── Free → paywall, demo only
├── "Upgrade" → billing.html
└── "Manage billing" → billing.html
Billing (billing.html) — PROTECTED
├── Shows current plan, status, renewal date
├── "Choose plan" → POST /api/create-checkout-session → Stripe Checkout
├── Stripe Checkout → success → billing.html?success=1
└── Stripe Checkout → cancel → billing.html?canceled=1
Stripe Webhook (POST /api/stripe-webhook)
└── Receives events from Stripe
└── Updates subscriptions table (status, plan, period_end, etc.)
└── Works even if user closes browser tab
| Status | Meaning | Access |
|---|---|---|
free |
No subscription | Demo only |
active |
Paid and current | Full access |
trialing |
Free trial active | Full access |
past_due |
Payment failed | Limited (shows warning) |
canceled |
Subscription canceled | Demo only |
unpaid |
Invoice unpaid | Demo only |
- No secrets on client — Stripe Secret Key and Service Role Key are only in Vercel serverless functions
- Server-side subscription check —
/api/subscription-statusvalidates the JWT and returns subscription data from the DB (not from the client) - Webhook reliability — If user closes browser after payment, the webhook still fires and updates the DB
- RLS protection — Users can only read their own subscription row; writes go through service_role in serverless functions
- Fallback — If the API is temporarily unavailable, the client falls back to direct Supabase RLS-protected query
- No empty error messages — Error banners only appear when there's an actual error message to show