A community hub of curated experiences — movement, discovery, connection — for people in their next chapter of life (50+). Members browse a curated catalog, reserve spots (free RSVP today, Stripe later), and partner-ticketed events capture the registration on-platform before handing off to the partner site. Behind it: a scraping pipeline that discovers events per market, human approval gates, a marketing stack (email campaigns, journeys, Meta ads), and an internal AI wiki.
| Layer | Tech |
|---|---|
| Frontend | Next.js 15 App Router on Vercel, Tailwind + shadcn/ui, recharts |
| Backend | Convex (database, functions, crons, file storage, HTTP actions) |
| Auth | Convex Auth — passwordless email OTP primary (ADR-009) + Google SSO; password kept as legacy sign-in only |
Resend via @convex-dev/resend component; marketing on a separate subdomain (ADR-012) |
|
| Ads/attribution | Meta pixel + Conversions API (ADR-013), first-touch attribution (ADR-011), first-party usage analytics |
| LLMs | OpenAI (extraction/validation/wiki/marketing), Perplexity (discovery), Serper (source discovery) |
| Observability | Sentry (frontend, ADR-014), errorLogs table for Convex failures, /admin/health |
| Testing | vitest + convex-test (unit), Playwright (e2e), GitHub Actions CI |
npm install
npm run dev # runs `next dev` + `convex dev` in parallelFirst run: convex dev provisions a dev deployment and writes
.env.local. Copy .env.local.example for the frontend vars, and set
Convex-side vars with npx convex env set KEY value (see the env table
below). There are two deployments — dev and prod — and each holds its own
env vars.
Bootstrap an admin (after signing up once through the UI):
npx convex run users:setRoleInternal '{"email":"you@example.com","role":"admin"}'Seeds: eventCategories:seed (21 default categories, admin-gated),
metrics:seedMockMetrics / attribution:seedMockAttribution (demo dashboards
— purge later with metrics:purgeMockMetrics), testing:seed (e2e fixtures,
requires ALLOW_TEST_SEED=true).
npm test # unit: reserve money-paths, registration derivation,
# event mutations, slot invariants, claims (convex-test)
npm run test:e2e:ui # Playwright with the step-through UI (watch the browser)
npm run test:e2e:headed # full visible-browser runE2E runs against its own Convex deployment (dev/e2e), provisioned once
and reused (ADR-035):
npm run e2e:up # create/push dev/e2e — the test:e2e scripts do this for you
npm run test:e2e
npm run e2e:status # which deployment, and whether the e2e flags are setNever set TEST_STATIC_OTP or ALLOW_TEST_SEED on your dev deployment.
While TEST_STATIC_OTP is set, every sign-in code is a published constant and
sign-in is refused for any address that is not @resend.dev; the suite also
deletes rows to rebuild its fixture world. The suite now refuses to run against
whatever .env.local names, for exactly that reason. (DEV_OTP_LOG=true on
your dev deployment is unrelated and recommended — it prints the real code to
the Convex log so you can sign in without a verified sending domain.)
| Deployment | Type | What it's for |
|---|---|---|
production |
prod | the real thing |
staging |
prod | pre-prod mirror; prod semantics (server errors hidden from clients, admin-only edits) |
local:… / dev/<you> |
dev | what npm run dev uses. Named in .env.local |
dev/e2e |
dev | the Playwright suite owns it: wiped and reseeded every run. Addressed by a scoped deploy key in .env.e2e.local, never by .env.local |
preview/<branch> |
preview | per-branch, auto-expiring |
CI (.github/workflows/): ci.yml runs lint/typecheck/unit on every push;
e2e.yml runs nightly + on demand against an anonymous local Convex backend it
provisions per run (so it needs no dev/e2e); backup.yml exports the prod
deployment nightly (30-day artifacts).
Vercel builds run npx convex deploy --cmd 'npm run build' (see
vercel.json), so functions/schema and frontend ship atomically — set
CONVEX_DEPLOY_KEY (production deploy key) in the Vercel env, scoped to
Production only.
Three environments (ADR-026): main → Vercel Production → Convex prod;
any PR → Vercel Preview → Convex staging, which is where non-technical
co-founders acceptance-test a feature before it merges. Staging is a long-lived
deployment (npx convex deployment create staging --type prod), not a per-PR
one, so it keeps its data — which is only safe with one PR open at a time.
Full setup, the two variables that silently break isolation, and the
prod-vs-staging env matrix: documentation/RUNBOOK.md → Environments.
app/
(user)/ public storefront: experiences catalog + detail, calendar,
blog, reserve/[slotId] (+ confirmed), story/[reservationId],
account, ambassador/[slotId], preferences (onboarding)
admin/ console: events (board/table + editor), curate wizard,
reservations, markets, categories, blog, stories,
campaigns, journeys, audience, discovery, crawler, logs,
usage, kpi, attribution, users
wiki/ internal AI wiki ("Data Brain", noindexed, auth-gated)
auth/ OTP / Google / legacy password sign-in
api/ ics/[reservationId], out/[eventId] (tracked partner
redirect), unsubscribe (RFC 8058)
- Storefront:
events,experiences(public projections),slots,reservations(internal + external registrationType),waitlist,stories,ambassadors,adminEvents(aggregates) - Pipeline (see
documentation/scraping-pipeline.md):marketSources,eventDiscoveryJobs,crawlQueue,actions/*(discover, crawl, extract, validate), human gates in/admin/curate+/admin/events - Marketing:
campaigns,emailCampaigns+campaignWorker,journeys,audience,metaAds,marketingImport - Analytics:
usage(first-party events + rollup + dashboard),computedMetrics(weekly KPI cron),metrics,attribution - Platform:
auth,users,emails,errorLogs,retention(nightly purges),migrations(@convex-dev/migrations ledger),crons(9 jobs),lib/(auth, limiter, registration, slots, reportError, …)
| Var | Where | Purpose |
|---|---|---|
CONVEX_DEPLOYMENT, NEXT_PUBLIC_CONVEX_URL |
.env.local only |
written by convex dev. NEXT_PUBLIC_CONVEX_URL must not exist in Vercel — convex deploy injects the right one per build, and a stored value overrides it, letting a preview frontend read the production backend (and sending staging analytics to prod via lib/usage.ts) |
NEXT_PUBLIC_CONVEX_SITE_URL |
.env.local / Vercel |
.convex.site origin for HTTP actions (usage ingest) |
NEXT_PUBLIC_SITE_URL |
Vercel | canonical origin (sitemap, JSON-LD, .ics) |
NEXT_PUBLIC_META_PIXEL_ID |
Vercel | Meta browser pixel (ADR-013) |
NEXT_PUBLIC_SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN |
Vercel | Sentry (ADR-014); no-op unset |
SITE_URL |
convex env | origin used inside emails |
OPENAI_API_KEY, OPENAI_MARKETING_MODEL, OPENAI_WIKI_MODEL |
convex env | LLM calls |
PERPLEXITY_API_KEY, SERPER_API_KEY |
convex env | discovery |
RESEND_API_KEY, RESEND_FROM, RESEND_MARKETING_FROM, RESEND_TEST_MODE, RESEND_WEBHOOK_SECRET |
convex env | email (ADR-008/012) |
UNSUBSCRIBE_SECRET, MARKETING_POSTAL_ADDRESS |
convex env | one-click unsubscribe + CAN-SPAM |
AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET |
convex env | Google SSO |
META_ACCESS_TOKEN, META_PIXEL_ID, META_AD_ACCOUNT_ID, META_PAGE_ID, META_TEST_EVENT_CODE |
convex env | Conversions API + insights import |
DEV_OTP_LOG |
convex env (dev only) | prints the real sign-in code to the Convex log — never on prod |
TEST_STATIC_OTP, ALLOW_TEST_SEED |
convex env (e2e deployment only) | set by the Playwright run and removed by its teardown. Never set by hand: TEST_STATIC_OTP blocks human sign-in on whatever deployment carries it (ADR-035) |
EMAIL_STUB |
convex env (test deployments only) | "true" routes every message into a stubbedEmails row instead of Resend, so a test run sends nothing while the send paths still run. Never on prod or staging — it swallows every member's mail (ADR-035) |
CONVEX_DEPLOY_KEY (in .env.e2e.local) |
generated, gitignored | scoped to dev/e2e; how the suite reaches it without touching .env.local |
CONVEX_DEPLOY_KEY |
Vercel | build-time Convex deploy. Two entries: the production key scoped to Production only, and the staging key scoped to Preview only. A production key visible to Preview makes every PR build fail (by design) — see RUNBOOK → Environments |
SCHEDULED_CRAWLS |
convex env (staging) | set to off — gates crawl-all-markets and discover-sources, the only two crons that spend on external APIs. Unset everywhere else |
DEV_EMAIL_REDIRECT |
convex env (dev + staging) | every outbound message goes to this one inbox. Never on prod — it silently swallows every member's mail |
CONVEX_PROD_DEPLOY_KEY |
GitHub secret | nightly backup export |
documentation/DECISIONS.md— ADR log (the "why"); newest firstdocumentation/RUNBOOK.md— deploy, rollback, backups, key rotation, cronsdocumentation/scraping-pipeline.md— pipeline behavior spec (versioned)documentation/browse-modes.md— the four browsing modes and every grouping rule behind them, with data-coverage baselines (versioned). Start here to change what a collection contains or what "a weekday morning" means.documentation/BRAND.md— voice/copy referencedocumentation/CHANGELOG.md— narrative history of big batchesdocumentation/TODO.md— intentionally deferred workCLAUDE.md— agent working conventions