Skip to main content

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:

ToolVersion / Notes
Node.js22 — CI runs on 22. Root package.json only requires >=20, but match CI to avoid surprises
pnpm10.14.0, pinned via packageManager in package.json. Install with corepack enable (comes with Node)
gitAny recent version
Convex CLINo install needed — invoked per-use via npx convex
WranglerNo 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:

ServiceWhat it's forDashboard
ConvexBackend/DB — project slappcardhttps://dashboard.convex.dev
ClerkAuth (roles: fan/artist/admin/affiliate/label)https://dashboard.clerk.com
CloudflareWorkers (frontend hosting) + R2 (audio/QR storage)https://dash.cloudflare.com
StripePayments (test mode for local/staging)https://dashboard.stripe.com
ResendTransactional emailhttps://resend.com
SentryError tracking — hard requirement, not optionalhttps://sentry.io
Cloudflare TurnstileBot protection on public formsPart 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.local itself with CONVEX_DEPLOYMENT + CONVEX_URL — in that case just copy the apps/web one.

Root .env.local (Convex CLI)​

VarWhat it's for
CONVEX_DEPLOYMENTWhich Convex deployment the CLI targets (e.g. slappcard-dev) — written by npx convex dev
CONVEX_URLThe deployment's API URL — written by npx convex dev; copy this value into VITE_CONVEX_URL

apps/web/.env.local (Vite, VITE_* vars)​

VarRequired?What happens if unsetWhere to get it
VITE_CONVEX_URLYes — nothing works without itConvex provider never connects; every query/mutation failsCopy CONVEX_URL from root .env.local
VITE_CLERK_PUBLISHABLE_KEYYes, for any auth-gated page (portals)Clerk provider skipped; sign-in unavailableClerk dashboard → API Keys (use the dev/staging Clerk app)
VITE_APP_ENVNo (defaults sensibly)Sentry environment tag + a few UI branches. Set development locally, staging/production in CI—
VITE_TURNSTILE_SITE_KEYNoTurnstile widgets just don't renderCloudflare dashboard → Turnstile
VITE_SENTRY_DSNNo locally, yes in staging/prodSentry becomes a no-op clientSentry → project settings
VITE_CAL_COM_EMBED_URLNoBooking CTA falls back / hidesCal.com embed settings
VITE_CAL_URLNoLegacy alias for the above — prefer VITE_CAL_COM_EMBED_URL—
VITE_CF_WEB_ANALYTICS_TOKENNoOptional manual beacon; prefer Cloudflare dashboard auto-inject—
VITE_INTRO_CARDNoControls which demo card code the marketing site links to—
VITE_POSTHOG_KEY / VITE_POSTHOG_HOSTNoPostHog fully inert. Autocapture/session recording are off by design — only explicit trackEvent(...) calls send anythingPostHog → 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:

VarUsed forRequired for
CLERK_JWT_ISSUER_DOMAINVerifying Clerk JWTs in auth.config.tsAny authenticated query/mutation
CLERK_SECRET_KEYClerk webhooks / admin API callsUser sync
ADMIN_EMAILSComma-separated allowlist — these emails always get role: admin regardless of Clerk metadataRecommended (bootstraps your own admin access)
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRETCheckout session creation / webhook signature verificationPurchases & fulfillment
RESEND_API_KEY / RESEND_FROM_EMAILTransactional email sendsEmail flows
TURNSTILE_SECRET_KEYServer-side Turnstile verificationOnly if VITE_TURNSTILE_SITE_KEY is set
SENTRY_DSN@sentry/node in Node actionsStaging/prod
APP_ENVSentry environment tag for backend eventsDefaults to "staging"
APP_BASE_URLAbsolute links in emails + QR code targetsEmail/QR flows
CARD_CLAIM_BASE_PATHBase path for card claim URLsDefaults to /c
STUDIO_ADMIN_KEYOptional secondary access key for the print-studio toolNo
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_WORKER_NAMEAdmin site-overview dashboard reads Cloudflare Analytics GraphQL API. Different credential than the GitHub Actions deploy token — only needs Account Analytics:ReadAdmin analytics widget only
R2_BUCKET / R2_ENDPOINT / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY@convex-dev/r2 — audio + QR storageYes, or audio upload/playback and QR generation fail
POSTHOG_API_KEY / POSTHOG_HOSTServer-side purchase_completed capture on Stripe webhook fulfillmentNo — 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:

  1. Writes CONVEX_DEPLOYMENT + CONVEX_URL to root .env.local.
  2. 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 (dev and prod), but only dev is 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 dev pushes changes to the Convex dev deployment as you edit convex/ — 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 change convex/schema.ts — the web app imports ConvexProvider and typed helpers from convex (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 into apps/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​

  1. Decide the type. Public data → query; writes → mutation; external services (Stripe, Resend, R2, Sentry) → action with "use node" (or "use v8" for lighter actions); webhooks → httpAction in convex/http.ts.

  2. 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.

  3. 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
},
});
  1. Validate args with v from convex/values — every function should declare its args validator; invalid args get rejected at the boundary.

  2. If the function can fail meaningfully, report it to Sentry — convex/lib/sentry.ts exports reportError(ctx, error, opts). Sentry is a hard requirement, not polish (see AGENTS.md for PII rules — never put raw fan emails in breadcrumbs/messages).

  3. TypeScript regenerates automatically. The CLI writes convex/_generated/ on save — you don't run anything. With npx convex dev running, the new function is live immediately; in the browser you call it as api.myModule.myFavoriteTracks(...).

Adding a page​

All routes live in apps/web/src/App.tsx (React Router v7):

  1. Create the page component in apps/web/src/pages/ (or src/pages/portals/ for portal sections).
  2. Import it in App.tsx and 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>.

  1. If the page needs backend data, add the Convex function first (above) and call it with the generated api import.
  2. If it's a marketing-page-style section, check src/sections/ for reusable sections before writing new markup.
  3. After changes, the regenerated inventory (docs/generated/) updates via pnpm docs:inventory — that's part of pnpm docs:dev / pnpm docs:build automatically.

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​

  1. 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).
  2. Don't commit secrets. .env* files are gitignored; double-check you haven't inlined a real key into a file you're adding.
  3. Remember Sentry. Don't ship a change that breaks Sentry wiring — it's a hard requirement (see AGENTS.md).
  4. Leave Front End Non React/ and docs/legacy-wix-archive/ alone unless your task explicitly touches them.

Branching model — day-to-day work happens on staging (the default branch):

BranchDeploys toConvex backend
stagingWorker slappcard-web → https://slappcard.amberprotocol.orgConvex dev deployment — every push auto-deploys
mainWorker 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​

DocPurpose
../README.mdThe full setup + every credential, dashboard, and gotcha — read this before your first deploy
STACK.mdLocked stack + rejected alternatives — read before proposing any technology change
ARCHITECTURE.mdService wiring, auth/stripe/R2 wiring, environment matrix
DOMAIN.mdEntities, access policies, abuse rules, Stripe mapping
CI.mdGitHub Actions workflows, exact Cloudflare token scopes
SELF_DOCS.mdThe self-updating docs system (Docusaurus + DeepSeek)
AGENTS.mdAI 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.