Dispense unique credit codes at hackathons, conferences and meetups.
Admins paste an eligible email list and a pool of codes. Attendees open /<slug>, sign in with Clerk (Google or email code), and receive a code if that verified address is on the list. The same email always gets the same code back.
- Next.js App Router, deployed on Vercel
- Convex for the database and claim transaction
- Clerk for sign-in
- Tailwind CSS
Admin UI is /admin. Claim URLs are /<slug>.
Do not commit .env.local. Never put API tokens in git.
- Create or claim a Clerk application (not the temporary keyless app).
- Enable Google and Email code. Leave passwords off.
- Add the Convex integration, or a JWT template named
convex. The template must include theemailclaim. - Copy the publishable key, secret key, and Issuer URL (
https://….clerk.accounts.dev).
Copy .env.example to .env.local and set:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
npx convex login
npx convex dev
That creates the project and writes NEXT_PUBLIC_CONVEX_URL.
On the Convex dashboard, Deployment Settings → Environment Variables:
CLERK_JWT_ISSUER_DOMAIN= the Clerk Issuer URLADMIN_EMAILS= the address you will sign in with (comma-separated if more than one)
Then:
npm run dev
convex/auth.config.ts reads CLERK_JWT_ISSUER_DOMAIN. convex/lib/auth.ts gates admin on ADMIN_EMAILS.
Against that live backend, on localhost, one event:
- Sign in as an
ADMIN_EMAILSaddress. Open/admin→ New event. Slugsmoke-1. - Eligible emails: that address plus a second address you control.
- Codes:
SMOKE-AandSMOKE-B. - Open
/smoke-1, sign in with the eligible address, confirm you get a code. - Refresh: same code. Admin claimed count stays 1.
- Sign in with an address that is not on the list: ineligible, no extra claim.
If that fails, fix it before touching Vercel.
Clerk production does not accept *.vercel.app. Use a real hostname.
- Create or open the production deployment.
- Set
CLERK_JWT_ISSUER_DOMAINandADMIN_EMAILSon production (the production Clerk issuer, not.accounts.dev). - Generate a production deploy key with
deployment:deploy.
- Production instance: Google + email code, Convex JWT template with
email. - Allowed origin: the custom domain.
- Sign-in and sign-up paths:
/sign-in,/sign-up.
vercel.json sets the build command to npm run build:vercel. That runs npx convex deploy --cmd 'npm run build' when CONVEX_DEPLOY_KEY is present, and a normal Next build otherwise so a holding page can still deploy.
-
Import this GitHub repo (or merge to
mainif the project already exists). -
Production environment variables:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY/CLERK_SECRET_KEY(prod)NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-inNEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-upNEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/CONVEX_DEPLOY_KEY= production deploy key, Production environment only
You do not need to set
NEXT_PUBLIC_CONVEX_URLby hand.npx convex deployinjects it for the Next build. -
Attach the custom domain. Add that host in Clerk. Redeploy once DNS is live.
-
On the live host: sign in, create one event, claim once, refresh, confirm the same code.
Skip Convex preview deployments until the production claim path is boring.
Cross-event duplicate flagging. When emails are uploaded (pasted, one per line or comma-separated), any address that already appears on another event's eligible list is not added directly — it goes to a "Flagged for review" section on the manage-event page, where an organiser approves or rejects each one individually. Both decisions are recorded in the audit log. Upload results report how many addresses were flagged.
App-wide email blacklist. Admins manage a blacklist at /admin/blacklist. A blacklisted address is rejected on every path that would add it to an event's eligible list after being blacklisted — uploads skip it (reported as N rejected (blacklisted)), and approving a flagged email for a blacklisted address fails with an error. Each rejected upload attempt is recorded and shown to event admins in a read-only "Blacklisted" card on the manage-event page. Addresses already on an event's list before being blacklisted are left untouched; un-blacklisting an address clears its recorded hits.
The attendee never types an email into Vend. claimForCurrentUser reads the verified email from the Clerk JWT, normalises it (trim + lowercase), and:
- Returns the existing code if that email already claimed on this event
- Refuses if the email is not on the list
- Assigns the next available code if one remains
- Returns
exhaustedif the pool is empty
Assignment is a single Convex mutation, so two people cannot take the same code.
npm test
Uses convex-test. No live Clerk or Convex project is required.