Per .cursor/rules/general-instructions.mdc:
face-app is a Vite app with no backend of its own — it calls the shared zooly-app backend over CORS.route.ts only does param parsing, auth, and errors; business logic lives in packages (@zooly/db access + the @zooly/team-leaderboard-srv service module).div gets a data-testid.@zooly/db; all access goes through the access layer (no table exports).| Face-app concept | Backing | Notes |
|---|---|---|
| Board (a league, the URL slug) | NEW merch_leaderboard | Own chrome + kill switch; selected by slug; default = world-cup. |
| BoardTeam (a team in a league) | NEW merch_board_team (leaderboardId → board, campaignId → store) | Belongs to one board; holds the per-board seedCount + stat. |
| Merch store (reusable face paint) | existing merch_campaign | Many BoardTeams → one store. |
| Per-board stat (% who did face paint) | COUNT of merch_leaderboard_stat_event + BoardTeam seedCount | pct = BoardTeam count / sum within the board; highest first. |
| BoardTeam image / colors | merch_board_team.flagImgUrl / mainColor / colors[] / facePaintImageUrls[] | Flag image, bar color, swatch, example photos. |
| Click target → "store of Spain" | BoardTeam's campaign storefront URL (built at runtime) | Jumps straight into selfie/upload. |
| Sponsor logo | per-board sponsorLogoUrl (optional) | None at launch. |
| Package | Role |
|---|---|
@zooly/team-leaderboard-client (packages/team-leaderboard/client) | Board screen, search, handoff, admin UI, lead forms |
@zooly/team-leaderboard-srv (packages/team-leaderboard/srv) | Board response, search, session link, completion credit |
@zooly/db (packages/db) | Schema + access layer for boards, teams, stat events, leads |
@zooly/consent (packages/consent) | Shared cookie-consent popup + useConsent hook |
@zooly/util (packages/util) | Consent state (cookie scoped to .zooly.ai) |
apps/zooly-app | Hosts the public + admin API routes |
merch_campaign (the actual stores) and the downstream merch experience (selfie → result).merchCampaignOpenWhere() to surface only live stores.merch_session — extended with the credited BoardTeam (board_team_id).apps/zooly-app/app/api/merch/cors.ts (+ ALLOWED_DOMAINS_CORS) and S3_BUCKET_URL asset resolution.requireAdmin middleware that powers merch-admin.MainApp.goToStore navigates to the campaign storefront URL (built at runtime from the campaign's accountSlug + slug) with ?bt=<boardTeamId>, honoring the board's storeLinkTarget (same / new tab).campaign-landing-page.tsx) reads ?bt= and, once the fresh session exists, POSTs to /api/merch/leaderboard/link → linkSessionToBoardTeam → setSessionBoardTeam (persists board_team_id on merch_session).ai/generate/route.ts calls recordCompletionFromSession (non-blocking) after generationCompletedAt. It reads the session's boardTeamId and inserts one stat event. It is a no-op for direct visits (no boardTeamId) and idempotent via unique(merchSessionId, eventType).