Slappcard — Developer Quickstart
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 + Vite SPA on Cloudflare Workers, Convex (DB/API/backend), Cloudflare R2 (audio + QR storage), Clerk (auth), Stripe (commerce), Resend (email), Sentry (required), Cloudflare Turnstile (bots), first-party affiliates built in Convex.
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 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 |
| Cloudflare | Workers (frontend hosting) + R2 (audio/QR storage) | 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 |
| Cloudflare Turnstile | Bot protection on public forms | Part of the Cloudflare dashboard |
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.
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 — Vite 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 (e.g. slappcard-dev) — written by npx convex dev |
CONVEX_URL | The deployment's API URL — written by npx convex dev; copy this value into VITE_CONVEX_URL |
apps/web/.env.local (Vite, VITE_* vars)
| Var | Required? | What happens if unset | Where to get it |
|---|---|---|---|
VITE_CONVEX_URL | Yes — nothing works without it | Convex provider never connects; every query/mutation fails | Copy CONVEX_URL from root .env.local |
VITE_CLERK_PUBLISHABLE_KEY | Yes, for any auth-gated page (portals) | Clerk provider skipped; sign-in unavailable | Clerk dashboard → API Keys (use the dev/staging Clerk app) |
VITE_APP_ENV | No (defaults sensibly) | Sentry environment tag + a few UI branches. Set development locally, staging/production in CI | — |
VITE_TURNSTILE_SITE_KEY | No | Turnstile widgets just don't render | Cloudflare dashboard → Turnstile |
VITE_SENTRY_DSN | No locally, yes in staging/prod | Sentry becomes a no-op client | Sentry → project settings |
VITE_CAL_COM_EMBED_URL | No | Booking CTA falls back / hides | Cal.com embed settings |
VITE_CAL_URL | No | Legacy alias for the above — prefer VITE_CAL_COM_EMBED_URL | — |
VITE_CF_WEB_ANALYTICS_TOKEN | No | Optional manual beacon; prefer Cloudflare dashboard auto-inject | — |
VITE_INTRO_CARD | No | Controls which demo card code the marketing site links to | — |
VITE_POSTHOG_KEY / VITE_POSTHOG_HOST | No | PostHog fully inert. Autocapture/session recording are off by design — only explicit trackEvent(...) calls send anything | PostHog → project settings; host defaults to https://us.i.posthog.com |
Minimal working apps/web/.env.local:
VITE_CONVEX_URL= # copy the CONVEX_URL value from root .env.local
VITE_CLERK_PUBLISHABLE_KEY= # Clerk dashboard → API Keys
VITE_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 dev deployment), not in a local file. Nothing crashes if they're unset — the relevant feature just returns a clear "not configured" error. 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 webhooks / admin API calls | User sync |
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 | Purchases & fulfillment |
RESEND_API_KEY / RESEND_FROM_EMAIL | Transactional email sends | Email flows |
TURNSTILE_SECRET_KEY | Server-side Turnstile verification | Only if VITE_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 |
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 the GitHub Actions deploy 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 and its dev deployment — this is the backend staging already uses. Don't create a new project. 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.
Multiple Convex deployments exist (
devandprod), but onlydevis actively used — treat it as "staging's backend," not a personal scratch deployment.
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 — frontend
pnpm dev
- Frontend: Vite dev server → http://localhost:3000
- Backend:
npx convex devpushes changes to the Convexdevdeployment as you editconvex/— no rebuild or restart needed.
pnpm dev is shorthand for pnpm --filter @slappcard/web dev. There's also pnpm dev:convex which does exactly the npx convex dev above.
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/ Vite React 19 SPA — everything user-facing
src/pages/ Public pages (Home, CardGate, Purchase, …)
src/pages/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/ AppProviders — Clerk + Convex provider wiring
src/lib/ Sentry, PostHog, and other client helpers
src/App.tsx All routes live here (React Router v7)
wrangler.jsonc Staging Worker config (slappcard-web)
wrangler.prod.jsonc Prod Worker config (slappcard-web-prod)
apps/docs/ Docusaurus site (reads repo-root docs/)
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, Clerk webhook)
packages/email/ React Email templates — stub only, not wired up yet
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-web, deploy-docs, update-docs (DeepSeek)
.githooks/pre-push Blocks pushes that fail typecheck
.env.example Convex CLI env 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 importsConvexProviderand typed helpers fromconvex(the npm package), and your page code gets fully typed queries/mutations. - 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. 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 SPA
pnpm typecheck:convex # just the backend
There is no automated test suite yet — typecheck is the only automated correctness gate. Treat pnpm typecheck passing as your minimum bar for any change.
Lint
pnpm lint # eslint on apps/web
Note: the repo currently has known pre-existing eslint failures (~30 files). Lint is not CI-gated. Don't block on it, 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"(or"use v8"for lighter actions); 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";
import { requireRole } from "./lib/roles";
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 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
All routes live in apps/web/src/App.tsx (React Router v7):
- Create the page component in
apps/web/src/pages/(orsrc/pages/portals/for portal sections). - Import it in
App.tsxand add a<Route>:
import MyNewPage from "@/pages/MyNewPage";
// inside <SentryRoutes>
<Route path="/my-new-page" element={<MyNewPage />} />
Portals use nested routes (/artist/*, /admin/*, /affiliate/*, /label/*) — those live in the portal components under src/pages/portals/, which render their own <Routes>.
- 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/sections/for reusable sections 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.md 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 staging (the default branch):
| Branch | Deploys to | Convex backend |
|---|---|---|
staging | Worker slappcard-web → https://slappcard.amberprotocol.org | Convex dev deployment — every push auto-deploys |
main | Worker slappcard-web-prod (workers.dev URL only) | Convex prod deployment — only when deliberately promoting |
Promote to prod with git push origin staging:main — an isolated Worker means a promotion can never disrupt staging.
9. Where to go next
| Doc | Purpose |
|---|---|
../README.md | 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 |
ARCHITECTURE.md | Service wiring, auth/stripe/R2 wiring, environment matrix |
DOMAIN.md | Entities, access policies, abuse rules, Stripe mapping |
CI.md | GitHub Actions workflows, exact Cloudflare token scopes |
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 ARCHITECTURE.md (how services wire together) → pick a small task in a single convex/ module or a single page in apps/web.