Slappcard — Developer Quickstart
As of 2026-08-13. Target audience: developers new to this repo. By the end of this guide you'll have the app running locally, understand how the pieces fit together, and know the standard day-to-day workflows. For the full "fresh machine, every credential" walkthrough see the README.
One-line summary of what this is: NFC/QR plastic cards that unlock private artist listen/download experiences. Fans scan a card (or its unique URL) to gate into that artist's content. Stack: React 19 + Next.js 16 App Router on Vercel, Convex (DB/API/backend), Cloudflare R2 (audio + QR storage), Clerk (auth), Stripe (commerce), Resend (email), Sentry (required), Cloudflare Turnstile (bots), PostHog (flags + analytics), first-party affiliates built in Convex. Cloudflare Workers host docs and MCP only — not the web app.
Live app URLs:
| Surface | URL | Git |
|---|---|---|
| Staging (launch / QA) | https://staging.slappcard.com | main |
| Development | https://development.slappcard.com | dev |
| Docs | Vercel slappcard-docs | docs/** + apps/docs |
The git branch named staging is retired. Do not push it.
1. Prerequisites
Local tools:
| Tool | Version / Notes |
|---|---|
| Node.js | 22 — CI runs on 22. Root package.json only requires >=20, but match CI to avoid surprises |
| pnpm | 10.14.0, pinned via packageManager in package.json. Install with corepack enable (comes with Node) |
| git | Any recent version |
| Convex CLI | No install needed — invoked per-use via npx convex |
| Wrangler | No install needed — npx wrangler. Only if you deploy docs or MCP Workers manually |
GitHub CLI (gh) | Convenient for secrets/workflow management; not required to develop locally |
Accounts you'll need access to. Ask a founder for an invite or credentials if you're missing any:
| Service | What it's for | Dashboard |
|---|---|---|
| Convex | Backend/DB — project slappcard | https://dashboard.convex.dev |
| Clerk | Auth (roles: fan/artist/admin/affiliate/label) | https://dashboard.clerk.com |
| Vercel | Next.js hosting (apps/web) | https://vercel.com |
| Cloudflare | R2 (audio/QR) + Turnstile + docs/MCP Workers — not the Next app | https://dash.cloudflare.com |
| Stripe | Payments (test mode for local/staging) | https://dashboard.stripe.com |
| Resend | Transactional email | https://resend.com |
| Sentry | Error tracking — hard requirement, not optional | https://sentry.io |
| PostHog | Feature flags + analytics (optional locally) | https://us.posthog.com |
For basic local development you really need: Convex (login required for npx convex dev) and Clerk (publishable key for auth-gated pages). Everything else degrades gracefully when unset. Keep http://localhost:3000 in the Clerk app's allowed origins.
2. Clone & install
git clone https://github.com/JamesFincher/Slappcard.git
cd Slappcard
corepack enable # ensures the pinned pnpm version (10.14.0) is used
pnpm install # also wires up the pre-push typecheck hook (via the `prepare` script)
pnpm install runs a prepare script that sets git config core.hooksPath .githooks. If the hook ever stops running, re-run it manually:
git config core.hooksPath .githooks
3. Environment setup
Two env files are copied into place; both are gitignored, so values never land in git:
cp .env.example .env.local # repo root — Convex CLI uses this
cp apps/web/.env.example apps/web/.env.local # frontend — Next.js reads this
If you've already run
npx convex dev(section 4), it creates root.env.localitself withCONVEX_DEPLOYMENT+CONVEX_URL— in that case just copy the apps/web one.
Root .env.local (Convex CLI)
| Var | What it's for |
|---|---|
CONVEX_DEPLOYMENT | Which Convex deployment the CLI targets — written by npx convex dev |
CONVEX_URL | The deployment's API URL — written by npx convex dev; copy this value into NEXT_PUBLIC_CONVEX_URL |
apps/web/.env.local (Next.js, NEXT_PUBLIC_* vars)
The web app reads NEXT_PUBLIC_* only. VITE_* names are leftover in comments / error copy; they are not read at runtime and are not in .env.example. Do not set VITE_* expecting the Next app to pick them up.
| Var | Required? | What happens if unset | Where to get it |
|---|---|---|---|
NEXT_PUBLIC_CONVEX_URL | Yes — nothing works without it | Convex provider never connects; every query/mutation fails | Copy CONVEX_URL from root .env.local |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Yes, for any auth-gated page (portals) | Clerk provider skipped; sign-in unavailable | Clerk dashboard → API Keys (use the development/staging Clerk app) |
NEXT_PUBLIC_APP_ENV | No (defaults to development) | Sentry environment tag + a few UI branches. Set development locally, staging/production in deploy | — |
NEXT_PUBLIC_TURNSTILE_SITE_KEY | No | Turnstile widgets just don't render | Cloudflare dashboard → Turnstile |
NEXT_PUBLIC_SENTRY_DSN | No locally, yes in staging/prod | Sentry becomes a no-op client | Sentry → project settings |
NEXT_PUBLIC_CAL_COM_EMBED_URL | No | Booking CTA falls back / hides | Cal.com embed settings |
NEXT_PUBLIC_CAL_URL | No | Legacy alias — prefer NEXT_PUBLIC_CAL_COM_EMBED_URL | — |
NEXT_PUBLIC_CF_WEB_ANALYTICS_TOKEN | No | Optional manual beacon; prefer Cloudflare dashboard auto-inject | — |
NEXT_PUBLIC_INTRO_CARD | No | Set 1 / true when you want the marketing site to link a seeded demo card | — |
NEXT_PUBLIC_POSTHOG_KEY / NEXT_PUBLIC_POSTHOG_HOST | No | PostHog fully inert. When set: named trackEvent(...) calls plus autocapture / session replay (inputs masked). Feature flags still default OFF | PostHog → project settings; host defaults to https://us.i.posthog.com. Browser traffic is proxied through /ingest |
NEXT_PUBLIC_APP_URL | No | Canonical site URL for metadata, robots.txt, sitemap.xml, and OG image URLs — defaults to https://staging.slappcard.com | Set http://localhost:3000 locally if you care about local absolute links |
Portal gating (apps/web/proxy.ts) also needs CLERK_SECRET_KEY in apps/web/.env.local. Without it, Clerk middleware is skipped and signed-out visitors are not redirected away from /admin, /artist, /affiliate, /label. That secret is a Convex dashboard var too; copy the same value into the Next env for local portal work.
Minimal working apps/web/.env.local:
NEXT_PUBLIC_CONVEX_URL= # copy the CONVEX_URL value from root .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= # Clerk dashboard → API Keys
NEXT_PUBLIC_APP_ENV=development
Everything else can stay empty for local dev — each unset var no-ops gracefully.
Convex backend env vars
These live in the Convex dashboard (Settings → Environment Variables on the deployment you linked), not in a local file. Nothing crashes if they're unset — the relevant feature just returns a clear "not configured" error. Presence-only readout: /admin/setup via convex/lib/envStatus.ts. Set them only for the features you're touching:
| Var | Used for | Required for |
|---|---|---|
CLERK_JWT_ISSUER_DOMAIN | Verifying Clerk JWTs in auth.config.ts | Any authenticated query/mutation |
CLERK_SECRET_KEY | Clerk Admin API / impersonation. No /clerk/webhook httpAction today — do not configure a Clerk webhook unless we add the route | User sync + portal gating locally |
ADMIN_EMAILS | Comma-separated allowlist — these emails always get role: admin regardless of Clerk metadata | Recommended (bootstraps your own admin access) |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET | Checkout session creation / webhook signature verification. Webhook URL is https://<deploy>.convex.site/stripe/webhook | Purchases & fulfillment |
RESEND_API_KEY / RESEND_FROM_EMAIL | Transactional email sends | Email flows |
TURNSTILE_SECRET_KEY | Server-side Turnstile verification (fail-closed in production) | Only if NEXT_PUBLIC_TURNSTILE_SITE_KEY is set |
SENTRY_DSN | @sentry/node in Node actions | Staging/prod |
APP_ENV | Sentry environment tag for backend events | Defaults to "staging" |
APP_BASE_URL | Absolute links in emails + QR code targets | Email/QR flows — set it per deployment so links match the host |
CARD_CLAIM_BASE_PATH | Base path for card claim URLs | Defaults to /c |
STUDIO_ADMIN_KEY | Optional secondary access key for the print-studio tool | No |
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_WORKER_NAME | Admin site-overview dashboard reads Cloudflare Analytics GraphQL API. Different credential than any GitHub Actions token — only needs Account Analytics:Read | Admin analytics widget only |
R2_BUCKET / R2_ENDPOINT / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY | @convex-dev/r2 — audio + QR storage | Yes, or audio upload/playback and QR generation fail |
POSTHOG_API_KEY / POSTHOG_HOST | Server-side purchase_completed capture on Stripe webhook fulfillment | No — skipped silently if unset |
Never commit
.env,.env.local, or secret values. Only variable names are documented in git.
4. Convex setup
Run this from the repo root:
npx convex dev
First run: you'll be prompted to log in (browser flow), then to pick/create a project and deployment. Choose the existing slappcard project — don't create a new one. Use the development deployment (the backend behind https://development.slappcard.com). This command:
- Writes
CONVEX_DEPLOYMENT+CONVEX_URLto root.env.local. - Stays running — it watches
convex/and live-pushes function changes to the deployment (hot reload for the backend).
Keep it running in its own terminal for the whole dev session. One-shot alternative (used by CI): npx convex dev --once.
Deployments are per environment (CONVEX_DEPLOY_KEY_DEV / _STAGING / _PROD). Treat the shared development deployment as the team backend, not a personal scratch project.
5. Running locally
You need two terminals:
# Terminal 1 — Convex backend (watch + live-push; from section 4, keep it running)
npx convex dev
# Terminal 2 — Next.js app
pnpm dev
- Frontend:
next dev -p 3000→ http://localhost:3000 (pnpm devispnpm --filter @slappcard/web dev) - Backend:
npx convex devpushes changes to the linked Convex deployment as you editconvex/— no rebuild or restart needed
There's also pnpm dev:convex, which is exactly npx convex dev.
Optional — seed demo data:
pnpm seed # runs convex/seed.ts via `npx convex run seed:seedDemo`
6. Project structure tour
Slappcard/ # single pnpm monorepo
apps/web/ Next.js 16 App Router — everything user-facing
app/ Routes (page.tsx shells + generateMetadata)
src/views/ Page implementations (Home, CardGate, portals, …)
src/views/portals/ Admin, Artist, Account, Affiliate, Label portals
src/components/ Shared UI + per-portal management components
src/admin-ui/ Themed UI kit for admin/artist backend desks only
src/providers/ Clerk + Convex provider wiring
src/lib/ Env, Sentry, PostHog, share-preview, and other client helpers
proxy.ts Clerk middleware (portal gating)
vercel.json Vercel Next.js build (monorepo)
apps/docs/ Docusaurus site (reads repo-root docs/) → Vercel slappcard-docs
apps/mcp/ MCP Worker (slappcard-mcp)
convex/ Backend — schema, queries, mutations, actions, httpActions
schema.ts Single source of truth for every table
lib/ Shared backend helpers (auth, roles, mint, collect, sentry, …)
_generated/ Auto-generated by the Convex CLI — do NOT hand-edit
*.ts One module per domain (cards.ts, orders.ts, affiliates.ts, …)
seed.ts Demo data seeder
http.ts httpActions (Stripe webhook only today)
packages/email/ React Email templates — stub; live HTML is in convex/lib/emailTemplates.ts
docs/ Canonical Markdown + Docusaurus content
docs/generated/ Machine inventory (routes, Convex modules, env names) — `pnpm docs:inventory`
Front End Non React/ Original static prototype. Leave untouched.
.github/workflows/ ci, deploy (Vercel + Convex), promote-*, deploy-docs, deploy-mcp, update-docs
.githooks/pre-push Blocks pushes that fail typecheck
.env.example Env name template (root)
Key things to know about the layout:
- Shared types flow from Convex into the frontend.
convex/_generated/is regenerated by the CLI whenever you changeconvex/schema.ts— the web app imports typedapihelpers from the generated client. - Every table is defined once, in
convex/schema.ts. Adding a field or table there is the first step of any backend change. - One domain per module.
convex/cards.ts,convex/orders.ts,convex/affiliates.ts, etc. — find the domain file before creating a new one. - Routes live in
apps/web/app/. Eachpage.tsxis a thin App Router shell; the interactive UI lives undersrc/views/. Front End Non React/is frozen — its design has already been ported intoapps/web. Don't build new features there.docs/legacy-wix-archive/contains real customer PII (names, emails, addresses). Reference-only, never copy into a public artifact.
7. Common dev workflows
Typecheck
pnpm typecheck # web + convex — the exact check the pre-push hook runs
pnpm typecheck:web # just the Next app
pnpm typecheck:convex # just the backend
Typecheck is the push and CI gate. There are Vitest unit tests under apps/web/src/__tests__/ and a Playwright script (pnpm e2e); neither is CI-gated today. Treat pnpm typecheck passing as your minimum bar for any change.
Lint
pnpm lint # eslint on apps/web
Lint is not CI-gated. Don't block on pre-existing failures, but don't add new lint errors either.
Adding a Convex function
-
Decide the type. Public data →
query; writes →mutation; external services (Stripe, Resend, R2, Sentry) →actionwith"use node"; webhooks →httpActioninconvex/http.ts. -
Find the right module.
convex/cards.ts,convex/orders.ts,convex/affiliates.ts, … — one module per domain. Create a new file only if the domain doesn't exist yet. -
Use the auth/role helpers for anything user-scoped. From
convex/lib/auth.ts/convex/lib/roles.ts:
import { query } from "./_generated/server";
import { getCurrentUserOrNull } from "./lib/auth";
export const myFavoriteTracks = query({
args: {},
handler: async (ctx) => {
const user = await getCurrentUserOrNull(ctx);
if (!user) return [];
// ...query with ctx.db
},
});
-
Validate args with
vfromconvex/values— every public function should declare its args validator; invalid args get rejected at the boundary. -
If the function can fail meaningfully, report it to Sentry —
convex/lib/sentry.tsexportsreportError(ctx, error, opts). Sentry is a hard requirement, not polish (seeAGENTS.mdfor PII rules — never put raw fan emails in breadcrumbs/messages). -
TypeScript regenerates automatically. The CLI writes
convex/_generated/on save — you don't run anything. Withnpx convex devrunning, the new function is live immediately; in the browser you call it asapi.myModule.myFavoriteTracks(...).
Adding a page
Routes are Next.js App Router, not React Router:
- Create the UI in
apps/web/src/views/(orsrc/views/portals/for portal sections). - Add a thin
page.tsxunderapps/web/app/<path>/:
import type { Metadata } from "next";
import MyNewPage from "@/views/MyNewPage";
export const metadata: Metadata = {
title: "My new page",
};
export default function RoutePage() {
return <MyNewPage />;
}
Portals use catch-all routes (/artist/[[...rest]], /admin/[[...rest]], /affiliate/[[...rest]], /label/[[...rest]]) — those already exist under app/ and render the portal views. Nested portal desks live inside the portal components, not as extra App Router trees.
- If the page needs backend data, add the Convex function first (above) and call it with the generated
apiimport. - If it's a marketing-page-style section, check
src/components// existing views before writing new markup. - After changes, the regenerated inventory (
docs/generated/) updates viapnpm docs:inventory— that's part ofpnpm docs:dev/pnpm docs:buildautomatically.
Docs
pnpm docs:inventory # regenerate docs/generated/* from code (no LLM)
pnpm docs:dev # Docusaurus on http://localhost:3001
pnpm docs:build # static site → apps/docs/build
There's also an autonomous DeepSeek-powered doc updater on push and after successful web deploy — see SELF_DOCS if you're curious, but don't hand-write docs/generated content; it's machine-generated.
8. Before you commit
- Run
pnpm typecheck. The pre-push hook (.githooks/pre-push) runs it and blocks the push if it fails. Fix the error locally rather than reaching for--no-verify— the hook is the actual gate (no branch protection is configured; CI re-runs the same check on GitHub). - Don't commit secrets.
.env*files are gitignored; double-check you haven't inlined a real key into a file you're adding. - Remember Sentry. Don't ship a change that breaks Sentry wiring — it's a hard requirement (see
AGENTS.md). - Leave
Front End Non React/anddocs/legacy-wix-archive/alone unless your task explicitly touches them.
Branching model — day-to-day work happens on dev:
| Branch | Deploys to | Convex backend |
|---|---|---|
dev | Vercel → https://development.slappcard.com | CONVEX_DEPLOY_KEY_DEV |
main | Vercel → https://staging.slappcard.com | CONVEX_DEPLOY_KEY_STAGING |
prod | Gated / unused until prod secrets exist. DNS for slappcard.com is not flipped | CONVEX_DEPLOY_KEY_PROD (unset) |
Push a feature branch, open a PR into dev. Promote to staging with the Promote to staging workflow (confirm=promote). Web deploys are vercel deploy --prod from Deploy stack — not Wrangler, not Cloudflare Workers. Docs and MCP still ship as Workers (deploy-docs.yml, deploy-mcp.yml).
9. Where to go next
| Doc | Purpose |
|---|---|
| README | The full setup + every credential, dashboard, and gotcha — read this before your first deploy |
| STACK.md | Locked stack + rejected alternatives — read before proposing any technology change |
| SERVICES.md | What ships with a deploy (Vercel, Convex, Clerk, R2, Workers for docs/MCP) |
| ARCHITECTURE.md | Service wiring, auth/stripe/R2 wiring, environment matrix |
| HOW_IT_WORKS.md | Current-code map of fan/artist journeys and portals |
| DOMAIN.md | Entities, access policies, abuse rules, Stripe mapping |
| CI.md | GitHub Actions workflows, Vercel + Convex deploy keys |
| SELF_DOCS.md | The self-updating docs system (Docusaurus + DeepSeek) |
| AGENTS.md | AI agent operating guide — conventions, do/don't rules, Sentry PII rules |
| docs/generated/ | Machine inventory (routes, Convex modules, env names) — regenerated via pnpm docs:inventory |
New here? Suggested order: finish this quickstart → skim DOMAIN.md (what the entities mean) → skim HOW_IT_WORKS.md / ARCHITECTURE.md (how services wire together) → pick a small task in a single convex/ module or a single view under apps/web/src/views/.