Skip to main content

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:

SurfaceURLGit
Staging (launch / QA)https://staging.slappcard.commain
Developmenthttps://development.slappcard.comdev
DocsVercel slappcard-docsdocs/** + apps/docs

The git branch named staging is retired. Do not push it.


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 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:

ServiceWhat it's forDashboard
ConvexBackend/DB — project slappcardhttps://dashboard.convex.dev
ClerkAuth (roles: fan/artist/admin/affiliate/label)https://dashboard.clerk.com
VercelNext.js hosting (apps/web)https://vercel.com
CloudflareR2 (audio/QR) + Turnstile + docs/MCP Workers — not the Next apphttps://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
PostHogFeature 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.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 — written by npx convex dev
CONVEX_URLThe 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.

VarRequired?What happens if unsetWhere to get it
NEXT_PUBLIC_CONVEX_URLYes — nothing works without itConvex provider never connects; every query/mutation failsCopy CONVEX_URL from root .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYYes, for any auth-gated page (portals)Clerk provider skipped; sign-in unavailableClerk dashboard → API Keys (use the development/staging Clerk app)
NEXT_PUBLIC_APP_ENVNo (defaults to development)Sentry environment tag + a few UI branches. Set development locally, staging/production in deploy
NEXT_PUBLIC_TURNSTILE_SITE_KEYNoTurnstile widgets just don't renderCloudflare dashboard → Turnstile
NEXT_PUBLIC_SENTRY_DSNNo locally, yes in staging/prodSentry becomes a no-op clientSentry → project settings
NEXT_PUBLIC_CAL_COM_EMBED_URLNoBooking CTA falls back / hidesCal.com embed settings
NEXT_PUBLIC_CAL_URLNoLegacy alias — prefer NEXT_PUBLIC_CAL_COM_EMBED_URL
NEXT_PUBLIC_CF_WEB_ANALYTICS_TOKENNoOptional manual beacon; prefer Cloudflare dashboard auto-inject
NEXT_PUBLIC_INTRO_CARDNoSet 1 / true when you want the marketing site to link a seeded demo card
NEXT_PUBLIC_POSTHOG_KEY / NEXT_PUBLIC_POSTHOG_HOSTNoPostHog fully inert. When set: named trackEvent(...) calls plus autocapture / session replay (inputs masked). Feature flags still default OFFPostHog → project settings; host defaults to https://us.i.posthog.com. Browser traffic is proxied through /ingest
NEXT_PUBLIC_APP_URLNoCanonical site URL for metadata, robots.txt, sitemap.xml, and OG image URLs — defaults to https://staging.slappcard.comSet 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:

VarUsed forRequired for
CLERK_JWT_ISSUER_DOMAINVerifying Clerk JWTs in auth.config.tsAny authenticated query/mutation
CLERK_SECRET_KEYClerk Admin API / impersonation. No /clerk/webhook httpAction today — do not configure a Clerk webhook unless we add the routeUser sync + portal gating locally
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 verification. Webhook URL is https://<deploy>.convex.site/stripe/webhookPurchases & fulfillment
RESEND_API_KEY / RESEND_FROM_EMAILTransactional email sendsEmail flows
TURNSTILE_SECRET_KEYServer-side Turnstile verification (fail-closed in production)Only if NEXT_PUBLIC_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 — set it per deployment so links match the host
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 any GitHub Actions 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 — don't create a new one. Use the development deployment (the backend behind https://development.slappcard.com). 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.

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 3000http://localhost:3000 (pnpm dev is pnpm --filter @slappcard/web dev)
  • Backend: npx convex dev pushes changes to the linked Convex deployment as you edit convex/ — 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 change convex/schema.ts — the web app imports typed api helpers 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/. Each page.tsx is a thin App Router shell; the interactive UI lives under src/views/.
  • 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 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

  1. Decide the type. Public data → query; writes → mutation; external services (Stripe, Resend, R2, Sentry) → action with "use node"; 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";

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 public function should declare its args validator; invalid args get rejected at the boundary.

  2. If the function can fail meaningfully, report it to Sentryconvex/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

Routes are Next.js App Router, not React Router:

  1. Create the UI in apps/web/src/views/ (or src/views/portals/ for portal sections).
  2. Add a thin page.tsx under apps/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.

  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/components/ / existing views 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 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 dev:

BranchDeploys toConvex backend
devVercel → https://development.slappcard.comCONVEX_DEPLOY_KEY_DEV
mainVercel → https://staging.slappcard.comCONVEX_DEPLOY_KEY_STAGING
prodGated / unused until prod secrets exist. DNS for slappcard.com is not flippedCONVEX_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

DocPurpose
READMEThe full setup + every credential, dashboard, and gotcha — read this before your first deploy
STACK.mdLocked stack + rejected alternatives — read before proposing any technology change
SERVICES.mdWhat ships with a deploy (Vercel, Convex, Clerk, R2, Workers for docs/MCP)
ARCHITECTURE.mdService wiring, auth/stripe/R2 wiring, environment matrix
HOW_IT_WORKS.mdCurrent-code map of fan/artist journeys and portals
DOMAIN.mdEntities, access policies, abuse rules, Stripe mapping
CI.mdGitHub Actions workflows, Vercel + Convex deploy keys
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 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/.