Play your round. Win big. Give back.
A subscription-driven golf scoring + charity fundraising + monthly prize draw platform. Golfers subscribe, log scores, pick a charity, and enter an automated monthly draw with real cash prizes.
Live: https://parforcharity.vercel.app
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Database | Supabase (PostgreSQL + RLS) |
| Auth | Supabase Auth + custom JWT hook |
| Payments | Stripe Checkout + Webhooks |
| Deployment | Vercel (region: lhr1) |
| Styling | Tailwind CSS + CVA |
| Language | TypeScript (strict mode) |
| Validation | Zod |
- Subscription system — Monthly / yearly plans via Stripe Checkout
- Score tracking — Rolling 5-score window; future dates and scores >1 year old rejected
- Charity directory — Subscribers choose a charity and contribution % (10–100%)
- Monthly prize draw — Automated draw engine with random and algorithmic modes
- Prize tiers — 5 matches (40% + jackpot), 4 matches (35%), 3 matches (25%)
- Jackpot rollover — Rolls over to next month if no 5-match winner
- Admin panel — Draw management, snapshot, publish, winner verification
- JWT role gating — Admin routes protected via custom Supabase access token hook
golf-charity-platform/
├── app/
│ ├── (auth)/ # Login, Signup pages
│ ├── (dashboard)/ # Subscriber dashboard (5 modules)
│ ├── (admin)/ # Admin panel
│ ├── api/ # Route handlers (scores, draws, charities, Stripe)
│ ├── charities/ # Public charity listing
│ └── page.tsx # Homepage
├── components/
│ ├── auth/ # LoginForm, SignupForm
│ ├── charities/ # CharityPicker
│ ├── dashboard/ # DashboardNav
│ ├── scores/ # ScoreEntry, ScoreList
│ └── ui/ # Button, Card, Badge, Input
├── lib/
│ ├── draws/ # Draw engine + snapshot
│ ├── stripe/ # Stripe client + webhook helpers
│ ├── supabase/ # Browser, server, middleware clients
│ └── utils.ts # Shared utilities
├── supabase/
│ └── migrations/ # 4 SQL migration files
└── types/ # TypeScript types for all 11 DB tables
git clone https://github.com/ankitnegi-dev/parforcharity.git
cd parforcharity/golf-charity-platform
npm installCopy .env.example to .env.local and fill in your values:
cp .env.example .env.local| Variable | Where to find it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase → Project Settings → API |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase → Project Settings → API |
SUPABASE_SERVICE_ROLE_KEY |
Supabase → Project Settings → API (server-only) |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Stripe → Developers → API keys |
STRIPE_SECRET_KEY |
Stripe → Developers → API keys |
STRIPE_WEBHOOK_SECRET |
Stripe → Webhooks → Signing secret |
STRIPE_MONTHLY_PRICE_ID |
Stripe → Products → Monthly plan Price ID |
STRIPE_YEARLY_PRICE_ID |
Stripe → Products → Yearly plan Price ID |
NEXT_PUBLIC_APP_URL |
Your deployed URL (e.g. https://parforcharity.vercel.app) |
Run these in order in Supabase Dashboard → SQL Editor:
supabase/migrations/001_initial_schema.sql ← 11 tables + triggers
supabase/migrations/002_rls_policies.sql ← Row-level security + is_admin()
supabase/migrations/003_charity_split.sql ← charity_selections + charity_donations
supabase/migrations/004_jwt_role_hook.sql ← JWT role embedding hook
After running 004, enable the hook in Supabase Dashboard → Authentication → Hooks → custom_access_token_hook.
npm run devOpen http://localhost:3000.
- Push to GitHub
- Go to vercel.com → Add New Project → import the repo
- Set Root Directory to
golf-charity-platform - Add all environment variables from Step 2 above
- Set Node.js version to
20.x - Click Deploy
Create a webhook endpoint in Stripe Dashboard → Developers → Webhooks:
- URL:
https://your-domain.vercel.app/api/stripe/webhook - Events:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,payment_intent.succeeded,payment_intent.payment_failed
Copy the signing secret into the STRIPE_WEBHOOK_SECRET env var and redeploy.
Run in Supabase → SQL Editor (replace with real email):
UPDATE auth.users
SET raw_app_meta_data = raw_app_meta_data || '{"role": "admin"}'
WHERE email = 'your-admin@email.com';
UPDATE public.users
SET role = 'admin'
WHERE email = 'your-admin@email.com';The user must sign out and back in for the new JWT claim to take effect. Admin panel is at /admin.
INSERT INTO public.charities (name, description, website_url, is_active) VALUES
('British Heart Foundation', 'Fighting heart and circulatory diseases.', 'https://www.bhf.org.uk', true),
('Macmillan Cancer Support', 'Supporting people living with cancer.', 'https://www.macmillan.org.uk', true),
('Age UK', 'Improving later life for older people.', 'https://www.ageuk.org.uk', true),
('RNLI', 'Saving lives at sea since 1824.', 'https://rnli.org', true),
('Mind', 'Mental health support and advocacy.', 'https://www.mind.org.uk', true);| Card | Use |
|---|---|
4242 4242 4242 4242 |
Successful payment |
4000 0000 0000 9995 |
Declined (insufficient funds) |
4000 0025 0000 3155 |
3D Secure required |
Expiry: any future date. CVC: any 3 digits.
The draw engine lives in lib/draws/engine.ts and supports two modes:
- Random — Fisher-Yates selection from a pool of 1–45
- Algorithmic — Frequency-weighted selection based on subscriber score history
Prize pool split per PRD §08:
| Tier | Match | Pool share |
|---|---|---|
| Jackpot | 5 numbers | 40% + accumulated jackpot |
| Second | 4 numbers | 35% |
| Third | 3 numbers | 25% |
If no 5-match winner, the jackpot rolls over to the following month.
| Priority | Action |
|---|---|
| 🔴 Critical | Rotate Supabase service role key (was exposed during development) |
| 🟡 Before first real draw | Replace Math.random() with crypto.randomInt() in draw engine |
| 🟡 Recommended | Replace is_admin() RLS helper with auth.jwt() ->> 'role' = 'admin' |
| 🟢 Nice to have | Add security headers (CSP, HSTS) to next.config.ts |
Private — all rights reserved. Built for the ParForCharity trainee assignment, March 2026.