- Node 22 (use
.nvmrc:nvm use) - pnpm (
npm i -g pnpm) - A Cloudinary account — sign up free. Free tier works for everything; the AWS Rekognition add-on has a free quota (see below).
- A Supabase project — supabase.com. Free tier works.
git clone https://github.com/your-org/kickoff-cards
cd kickoff-cards
nvm use
pnpm installCopy the example file and fill in your values:
cp .env.example .env.local| Variable | Where to find it |
|---|---|
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME |
Cloudinary dashboard → Cloud name |
NEXT_PUBLIC_CLOUDINARY_API_KEY |
Cloudinary dashboard → API keys — public identifier, required in signed upload requests |
CLOUDINARY_API_SECRET |
Cloudinary dashboard → API keys |
CLOUDINARY_WEBHOOK_URL |
See step 4 (tunnel URL) |
NEXT_PUBLIC_SUPABASE_URL |
Supabase → Project Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Supabase → Project Settings → API → Publishable key |
SUPABASE_SECRET_KEY |
Supabase → Project Settings → API → Secret key |
CLOUDINARY_API_SECRETandSUPABASE_SECRET_KEYare server-only — notNEXT_PUBLIC_, never sent to the browser. The API key (NEXT_PUBLIC_CLOUDINARY_API_KEY) is a public identifier; Cloudinary requires it alongside the signature in every upload request. It is not a secret.
Open your Supabase SQL editor (Dashboard → SQL editor → New query), paste the contents of supabase/schema.sql, and run it.
This creates all tables, RLS policies, the leaderboard view, and enables Realtime on cards and battles in one go.
The app uses guest (anonymous) auth — no email or SMTP required.
In your Supabase dashboard: Authentication → Sign In / Providers → toggle "Allow anonymous sign-ins"
One Cloudinary add-on is required:
- AWS Rekognition — async content moderation on every upload. Has a free quota; no billing required to get started.
Background removal uses e_background_removal, a standard Cloudinary transformation — no add-on needed.
Enable it in your Cloudinary dashboard under Add-ons.
The six nation card template images must be in your Cloudinary account before cards can be created. Upload them with the provided script:
pnpm upload-templates --dir ./path/to/template-imagesExpected files in the directory: arg.png, bra.png, fra.png, eng.png, spa.png, ger.png (.jpg and .webp also accepted).
The script sets public_id: "templates/{nation}" and asset_folder: "templates" explicitly, which works correctly regardless of whether your Cloudinary account uses Fixed or Dynamic folder mode. Do not upload templates via the Cloudinary dashboard — the auto-generated public_id will be a UUID and card composition URLs will 404.
Cloudinary calls your webhook URL after async moderation. In local dev the URL must be publicly reachable HTTPS.
Use Cloudflare Tunnel (no account needed):
cloudflared tunnel --url http://localhost:3000Copy the generated URL and set it in .env.local:
CLOUDINARY_WEBHOOK_URL=https://random-name.trycloudflare.com/api/cloudinary/webhook
pnpm devOpen http://localhost:3000. Click Play as guest → choose a handle → upload a photo to create your first card. The leaderboard and battle pages are empty until at least two users each have a card.
| Command | Description |
|---|---|
pnpm dev |
Start development server (Turbopack) |
pnpm build |
Production build + TypeScript check |
pnpm test |
Run Vitest unit tests |
pnpm lint |
ESLint |