API Reference

Public (CORS) and admin (requireAdmin) endpoints

All routes live in apps/zooly-app/ and are thin routers — they parse params, handle auth/CORS/errors, and delegate to @zooly/team-leaderboard-srv or @zooly/db. They reuse the shared CORS helper apps/zooly-app/app/api/merch/cors.ts (getCorsHeaders / handleOptions) with origins from ALLOWED_DOMAINS_CORS.

Public API (CORS, no auth)

Base: apps/zooly-app/app/api/merch/leaderboard/

GET /api/merch/leaderboard/:board

Returns one board (league) by slug: its chrome + its BoardTeams, ordered by count desc (count = seedCount + event COUNT).

  • Each item: { boardTeamId, boardTeamSlug, name, flagImgUrl, mainColor, colors[], facePaintImageUrls[], count, pct, target }.
  • pct is the share within this board.
  • target is { kind: "store", url }, where url is the campaign-derived store URL (built at runtime from the linked campaign), or null when the BoardTeam has no live campaign.
  • Unknown/empty slug resolves to the default board (isDefault = world-cup). Slug matching is case-insensitive and slug-normalized (nba-finals ↔ "NBA Finals").
  • 404 ("Board not found") when the requested board does not exist or is inactive.

Handler: apps/zooly-app/app/api/merch/leaderboard/[board]/route.tsbuildBoardResponse(slug).

GET /api/merch/leaderboard/search?q=

Server-side autocomplete (min 3 chars) returning matching boards and BoardTeams (by name / searchTerms). Each team result carries its leaderboardSlug so the client can switch boards or jump to the team.

Handler: apps/zooly-app/app/api/merch/leaderboard/search/route.tssearchLeaderboard(q).

POST /api/merch/leaderboard/link

Body { sessionId, boardTeamId }. Persists the credited BoardTeam on merch_session (called by the merch store landing when arriving with ?bt=).

Handler: apps/zooly-app/app/api/merch/leaderboard/link/route.tslinkSessionToBoardTeam.

POST /api/merch/leaderboard/stat

Credits a completion for a session by inserting one stat event for its boardTeamId; idempotent via unique(merchSessionId, eventType).

As built, crediting is triggered server-side from ai/generate (after generationCompletedAt) via recordCompletionFromSession, not from the client. This endpoint remains for manual/explicit crediting.

Handler: apps/zooly-app/app/api/merch/leaderboard/stat/route.ts.

Admin API (requireAdmin + CORS)

Base: apps/zooly-app/app/api/merch/admin/. Thin routers calling @zooly/db directly. All require the same zooly-auth SSO + requireAdmin middleware as merch-admin.

Method + pathPurpose
GET /leaderboardsList all boards
POST /leaderboardsCreate a board
GET /leaderboards/[id]Get one board
PATCH /leaderboards/[id]Update a board
DELETE /leaderboards/[id]Delete a board
GET /leaderboards/[id]/teamsList a board's teams
POST /leaderboards/[id]/teamsCreate a team under a board
GET /leaderboards/[id]/teams/[teamId]Get one team
PATCH /leaderboards/[id]/teams/[teamId]Update a team
DELETE /leaderboards/[id]/teams/[teamId]Delete a team
GET /leadsPaginated lead submissions (kind filter + search)

Files:

  • apps/zooly-app/app/api/merch/admin/leaderboards/route.ts
  • apps/zooly-app/app/api/merch/admin/leaderboards/[id]/route.ts
  • apps/zooly-app/app/api/merch/admin/leaderboards/[id]/teams/route.ts
  • apps/zooly-app/app/api/merch/admin/leaderboards/[id]/teams/[teamId]/route.ts
  • apps/zooly-app/app/api/merch/admin/leads/route.ts

Service layer (@zooly/team-leaderboard-srv)

packages/team-leaderboard/srv/src/

  • build-board-response.tsbuildBoardResponse(slug): resolves board (or default), composes chrome + BoardTeams, builds each target.url at runtime from the linked campaign (buildStoreUrl(accountSlug, campaignSlug), null when no live campaign), ordering, and stat formatting.
  • search-leaderboard.tssearchLeaderboard(q): board + BoardTeam autocomplete (min 3 chars).
  • link-session.tslinkSessionToBoardTeam(sessionId, boardTeamId): wraps setSessionBoardTeam.
  • record-completion.tsrecordCompletionFromSession(sessionId): reads the session's boardTeamId and records the stat; wired into ai/generate after generationCompletedAt (no-op when no boardTeamId).
  • index.ts — barrel export.

DB access functions

packages/db/src/access/merch/merch-leaderboard.ts (exported from packages/db/src/index.ts; no table exports):

  • Read: getBoardBySlug, getDefaultBoard, listBoardTeams, searchBoardsAndTeams.
  • Stat / session: recordCompletion, setSessionBoardTeam, getSessionBoardTeamId.
  • Seed/import: upsertBoard, upsertBoardTeam.
  • Admin CRUD: listAllBoards, getBoardById, createBoard, updateBoard, deleteBoard (with isDefault transaction), listAllBoardTeamsAdmin, getBoardTeamById, createBoardTeam, updateBoardTeam, deleteBoardTeam.
  • Leads: listLeadSubmissions (packages/db/src/access/leadSubmission.ts).