API Routes Map & Audit
Last Updated: August 20, 2026
Scope: All route handlers under src/app/api/ — 83 endpoints.
Overview
theunparty.app grew organically as a solo build, and the API surface is now large
enough that it needs a single, shared picture before the user‑journey "game logic"
gets wired on top of it. This document is that picture: what each route does, who
calls it, how it's protected, and which routes are not wired to anything yet.
Every folder under src/app/api/<em></em> that contains a route.ts/route.tsx becomes a
live HTTP endpoint (Next.js App Router). A [param] folder is a dynamic segment
(e.g. book/[bookId]). Two files in the tree are not endpoints:
book/[bookId]/route-old.ts (legacy, dead) and book/types.ts (shared types).
“This file is a hand‑maintained audit. When you add, remove, or re‑wire a route,”
“update the matching table and the Audit findings section.”
---
Visual map
How traffic flows: who calls the API → which domain handles it → where the data lives.
Rendering diagram…
---
Legend
Wiring — is the route actually reached, and by whom?
| Mark | Meaning |
|---|---|
| 🟢 | Called from the app UI (pages / components) |
| 🧭 | Reached via navigation or a <meta> / <img> URL (not fetch) |
| ⏰ | Triggered by Vercel Cron (vercel.json) |
| 🪝 | External signed POST (webhook or Vercel drain) |
| 📤 | External client using an API key |
| 🛠 | Ops / manual tool (run by hand, not wired to UI) |
| 🔴 | No caller found in the repo (orphaned — wire it or delete it) |
<meta> / <img> URL (not fetch)vercel.json)Auth — how the route is protected.
| Mark | Meaning |
|---|---|
| 🌐 | Public — no auth |
| 👤 | Signed‑in user (Clerk auth()) |
| 🛡️ | Admin role (requireAdmin()) |
| 🔑 | API key (STORY_API_KEY header) |
| ⏰ | Cron secret (CRON_SECRET bearer) |
| 🔏 | HMAC signature (webhook / drain) |
| ⚠️ | Gap — admin route that checks sign‑in only, not the admin role |
| ⛔ | Gap — mutating route with no auth at all |
auth())requireAdmin())STORY_API_KEY header)CRON_SECRET bearer)---
At a glance
| Domain | Count | Primary store |
|---|---|---|
| Journey engine | 13 | Postgres (+ 1 in‑memory) |
| Analytics & tracking | 6 | Postgres |
| Admin console | 20 | Postgres |
| Content | 8 | Postgres / disk |
| Commerce | 6 | Polar / Stripe + Postgres |
| UNPPP & build data | 6 | Postgres / GitHub / static |
| Media & assets | 14 | Filesystem / Blob / Apple |
| Messaging & search | 3 | Slack / Loops / Postgres |
| Integrations (cron, webhooks, drains) | 7 | Postgres / GitHub |
| Total | 83 |
---
1. Journey engine (game logic)
The core loop the user journey is built on: steps (tasks), the questions and challenges attached to them, and founder onboarding. This is the surface the new game logic will wire into, so its gaps matter most.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/steps | GET POST PUT DELETE | 👤 | List / manage the caller's steps (filter, paginate) | 🟢 |
/api/steps/check-completion | GET POST | 👤 | Re‑evaluate a step against its requirements and complete it | 🟢 |
/api/steps/recommendations | GET | 👤 | Suggest next steps from the user's responses | 🟢 |
/api/steps/[stepId]/complete | POST DELETE | 👤 | Manually mark a step complete / incomplete | 🔴 |
/api/steps/record-action | POST | 👤 | Record a named action and auto‑complete matching action‑steps | 🔴 |
/api/questions | GET POST | 👤 | Get a category's questions; save a response | 🟢 |
/api/questions/categories | GET | 👤 | List question categories | 🟢 |
/api/questions/[questionId]/response | GET | 👤 | The caller's saved response to one question | 🟢 |
/api/challenges/[challengeId]/vote | GET POST | 👤 | Save / read the caller's poll vote | 🟢 |
/api/challenges/[challengeId]/results | GET | 🌐 | Aggregated poll results (counts, percentages) | 🟢 |
/api/founders/foundersquestions | GET | 👤 | Founder questions from the DB | 🔴 |
/api/founders/responses | GET | 👤¹ | The founder's progress on founder questions | 🔴 |
/api/score | GET POST | 🌐 | Update / reset a score kept in memory only | 🔴 |
/api/steps/api/steps/check-completion/api/steps/recommendations/api/steps/[stepId]/complete/api/steps/record-action/api/questions/api/questions/categories/api/questions/[questionId]/response/api/challenges/[challengeId]/vote/api/challenges/[challengeId]/results/api/founders/foundersquestions/api/founders/responses/api/score¹ founders/responses is restricted to a single identity via FOUNDER_USER_ID, not a Clerk role.
---
2. Analytics & tracking
Records the journey so admins can see progress and retention. analytics/vercel is a
drain endpoint and is listed again under Integrations.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/analytics/session | POST | 👤 | Start / track a user session | 🟢 |
/api/analytics/user-progress | GET POST | 👤 | Get or update the user's profile progress | 🟢 |
/api/analytics/idea-progress | POST | 👤 | Upsert idea‑progression completion rate | 🟢 |
/api/analytics/metrics | GET | 👤 | Platform‑level metrics | 🟢 |
/api/analytics/retention | GET | 👤 | Retention metrics (active users by window) | 🔴 |
/api/analytics/vercel | POST | 🔏 | Ingest the Vercel analytics drain | 🪝 |
/api/analytics/session/api/analytics/user-progress/api/analytics/idea-progress/api/analytics/metrics/api/analytics/retention/api/analytics/vercel---
3. Admin console
Backs the /admin UI. Authorization here is inconsistent — see
Audit finding B. 🛡️ = proper admin‑role
check; ⚠️ = sign‑in only (any logged‑in user passes); ⛔ = no check at all.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/admin/articles | GET POST | 🛡️ | List / create editorial articles | 🟢 |
/api/admin/articles/[id] | GET PUT DELETE | 🛡️ | Read / update / delete an article | 🟢 |
/api/admin/categories | GET POST | 🛡️ | List / create step categories | 🟢 |
/api/admin/steps | GET POST | 🛡️ | List / create steps | 🟢 |
/api/admin/steps/[id] | GET PUT DELETE | 🛡️ | Read / update / delete a step | 🟢 |
/api/admin/steps/[id]/visibility | PATCH POST | ⛔ | Set / toggle step visibility | 🟢 |
/api/admin/steps/bulk-delete | POST | 🛡️ | Bulk delete steps | 🟢 |
/api/admin/steps/sync-questions | POST | 🛡️ | Sync founder questions into the DB | 🛠 |
/api/admin/questions | GET POST | ⚠️ | List / create questions | 🟢 |
/api/admin/questions/[id] | GET PUT DELETE | ⚠️ | Read / update / delete a question | 🟢 |
/api/admin/questions/upload | GET POST | 🛡️ | Bulk upload questions with Zod validation | 🛠 |
/api/admin/question-responses | GET | ⚠️ | Every user's question responses (Clerk‑enriched) | 🟢 |
/api/admin/submissions | GET POST | 🛡️ | List / create user submissions | 🟢 |
/api/admin/submissions/[id] | GET PUT DELETE | 🛡️ | Review a submission | 🟢 |
/api/admin/problem-worksheets | GET | 🛡️ | List submitted problem worksheets | 🟢 |
/api/admin/repositories | GET POST PUT DELETE | ⚠️ | Manage tracked GitHub repositories | 🟢 |
/api/admin/user-progress | GET | 🛡️ | Per‑user progress overview | 🟢 |
/api/admin/dashboard-stats | GET | ⚠️ | Aggregate dashboard stats | 🟢 |
/api/admin/db-stats | GET | ⚠️ | Row counts per table | 🟢 |
/api/admin/observability | GET | ⚠️ | Trace / analytics observability data | 🟢 |
/api/admin/articles/api/admin/articles/[id]/api/admin/categories/api/admin/steps/api/admin/steps/[id]/api/admin/steps/[id]/visibility/api/admin/steps/bulk-delete/api/admin/steps/sync-questions/api/admin/questions/api/admin/questions/[id]/api/admin/questions/upload/api/admin/question-responses/api/admin/submissions/api/admin/submissions/[id]/api/admin/problem-worksheets/api/admin/repositories/api/admin/user-progress/api/admin/dashboard-stats/api/admin/db-stats/api/admin/observability---
4. Content
Editorial articles, stories, and the dynamic social image. articles/stories are
public to read but require STORY_API_KEY to write or to pull the full external feed.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/articles | GET POST | 🌐 / 🔑 | List published articles (GET); create (POST, key) | 🟢 |
/api/articles/[slug] | GET PUT | 🌐 / 🔑 | Read / update an article by slug | 🟢 |
/api/articles/[slug]/thoughts | GET POST | 👤 | Reader "thoughts" on an article | 🟢 |
/api/articles/unppp/[unpppId] | GET | 🌐 | Markdown articles for a UNPPP | 🟢 |
/api/stories | GET | 🔑 | Full story feed for external sync | 📤 |
/api/stories/all | GET | 🌐 | Batched fetch of all stories (client bootstrap) | 🟢 |
/api/stories/[storyId] | GET PUT | 🌐 / 🔑 | Read / update one story | 🟢 |
/api/og | GET | 🌐 | Dynamic Open Graph image (ImageResponse) | 🧭 |
/api/articles/api/articles/[slug]/api/articles/[slug]/thoughts/api/articles/unppp/[unpppId]/api/stories/api/stories/all/api/stories/[storyId]/api/ogImageResponse)---
5. Commerce
Legacy book sales (Stripe) plus the current Polar checkout / pricing path.
webhook/polar also appears under Integrations.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/book | GET POST | 🌐 | List books; create a book (Zod) | 🟢 |
/api/book/[bookId] | POST | 🌐 | Claim / purchase a book | 🟢 |
/api/book/payment-intent | POST | 🌐 | Create a Stripe payment intent | 🟢 |
/api/checkout | GET | 🌐 | Redirect into a Polar checkout session | 🧭 |
/api/unppp/price | GET | 🌐 | Live Polar product price (cached 1h) | 🟢 |
/api/webhook/polar | POST | 🔏 | Polar payment webhook (order → fulfillment) | 🪝 |
/api/book/api/book/[bookId]/api/book/payment-intent/api/checkout/api/unppp/price/api/webhook/polar---
6. UNPPP & build data
Read‑only data feeding the status dashboard and priority views. Three overlapping
priority/effort endpoints exist — only priority-score is wired; see
Audit finding A.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/unppp/priority-score | GET | 🌐 | Live UNPPP priority scores (Prisma + percentiles, cached) | 🟢 |
/api/unppp/priority | GET | 🌐 | Static UNPPP priority data (force-static) | 🔴 |
/api/unppp/effort | GET | 🌐 | UNPPP effort / timeline data | 🔴 |
/api/theunpartybuilder/scores | GET | 🌐 | Builder assumption / similarity scores | 🔴 |
/api/status | GET | 🌐 | Build‑status summary from priority scores | 🔴 |
/api/repo-data | GET HEAD | 🌐² | GitHub repo metadata (GitHub App creds) | 🟢 |
/api/unppp/priority-score/api/unppp/priorityforce-static)/api/unppp/effort/api/theunpartybuilder/scores/api/status/api/repo-data² repo-data is unauthenticated but reads through server‑side GitHub App credentials.
---
7. Media & assets
SVG library management, generic uploads, Apple Music tokens, and cached brand assets. The SVG routes read/write the local filesystem, which is ephemeral on Vercel — see Audit finding C.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/get-svgs | GET | 🌐 | List SVG files in public/svgs | 🟢 |
/api/get-trashed-svgs | GET | 🌐 | List trashed SVGs | 🟢 |
/api/upload-svg | POST | 🌐 | Upload an SVG to public/svgs | 🟢 |
/api/delete-svg | DELETE | 🌐 | Move an SVG to trash | 🟢 |
/api/rename-svg | POST | 🌐 | Rename an SVG file | 🟢 |
/api/restore-svg | POST | 🌐 | Restore an SVG from trash | 🟢 |
/api/add-tag | POST | 🌐 | Tag an SVG (writes tags.json) | 🟢 |
/api/get-icon | GET | 🌐 | Icon lookup — mock/stub | 🔴 |
/api/upload | POST | 🌐 | Generic file upload (Vercel Blob) | 🟢 |
/api/brand-colors | GET | 🌐 | Brand palette from theunpartybrand (cached 1h) | 🟢 |
/api/html/[filename] | GET | 🌐 | Serve a public/*.html file | 🟢 |
/api/musickit-token | GET | 🌐 | Apple MusicKit developer token | 🟢 |
/api/music/metadata | GET | 🌐 | Apple Music dev token — duplicates musickit-token | 🟢 |
/api/music/mood-enrich | GET | 🌐 | Match MOOD commits to Apple Music (ops helper) | 🛠 |
/api/get-svgspublic/svgs/api/get-trashed-svgs/api/upload-svgpublic/svgs/api/delete-svg/api/rename-svg/api/restore-svg/api/add-tagtags.json)/api/get-icon/api/upload/api/brand-colorstheunpartybrand (cached 1h)/api/html/[filename]public/*.html file/api/musickit-token/api/music/metadatamusickit-token/api/music/mood-enrich---
8. Messaging & search
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/search | GET POST | 🌐 | Hybrid search (local + theunpartyapi) | 🟢 |
/api/chat/send | POST | 🌐 | Send a chat message to Slack | 🟢 |
/api/problem/send | GET POST | 👤 | Submit a problem worksheet (Loops email + DB) | 🟢 |
/api/searchtheunpartyapi)/api/chat/send/api/problem/send---
9. Integrations (cron, webhooks, drains)
Machine‑to‑machine endpoints. These are supposed to have no UI caller — they are driven by Vercel Cron, external webhooks, or Vercel drains, and each is signed/secret‑gated.
| Endpoint | Methods | Auth | Purpose | Wiring |
|---|---|---|---|---|
/api/cron/db-cleanup | GET | ⏰ | Prune analytics rows older than 30 days | ⏰ |
/api/cron/snapshot-unppp-pageviews | GET | ⏰ | Snapshot pageviews JSON via auto‑merged PR | ⏰ |
/api/cron/snapshot-build-priority | GET | ⏰ | Snapshot build‑priority JSON via PR | ⏰ |
/api/cron/snapshot-connect | GET | ⏰ | Snapshot connect JSON via PR | ⏰ |
/api/cron/snapshot-about-cost | GET | ⏰ | Snapshot about‑cost JSON via PR | ⏰ |
/api/webhook/polar | POST | 🔏 | Polar payment webhook | 🪝 |
/api/github/webhook | POST | 🔏 | GitHub App webhook (repo events → Loops) | 🪝 |
/api/analytics/vercel | POST | 🔏 | Vercel analytics drain ingest | 🪝 |
/api/drains/traces | GET POST | 🔏 | Vercel OTLP trace drain ingest | 🪝 |
/api/cron/db-cleanup/api/cron/snapshot-unppp-pageviews/api/cron/snapshot-build-priority/api/cron/snapshot-connect/api/cron/snapshot-about-cost/api/webhook/polar/api/github/webhook/api/analytics/vercel/api/drains/traces_Cron schedules live in vercel.json._
_Count note: webhook/polar and analytics/vercel are tallied under their primary
domains (Commerce and Analytics) in At a glance and repeated here only to
show the full machine‑to‑machine surface — so this table lists 9 rows for 7 unique
Integrations‑owned endpoints._
---
Audit findings
A. Routes with no caller (orphans)
No fetch, navigation, or external trigger references these anywhere in the repo.
Each is a decision: wire it into the journey, or delete it.
| Endpoint | Likely reason | Suggested action |
|---|---|---|
/api/steps/[stepId]/complete | Superseded by steps/check-completion (auto‑complete) | Confirm, then remove or wire as the manual override |
/api/steps/record-action | Intended hook — its own doc says "call this wherever an action happens" | Wire into real actions (e.g. connected_github) |
/api/analytics/retention | Dashboard never added | Wire into the admin dashboard or remove |
/api/founders/foundersquestions | App reads static founders/foundersquestions.ts instead | Remove or repoint to the DB source |
/api/founders/responses | No consumer | Remove or wire into the founder view |
/api/score | In‑memory only — resets every cold start, never persisted | Remove, or back with Postgres if scores are real |
/api/unppp/priority | Superseded by unppp/priority-score | Remove the static duplicate |
/api/unppp/effort | No consumer | Wire into the status/effort view or remove |
/api/theunpartybuilder/scores | No consumer | Wire into the builder view or remove |
/api/status | Page renders from data directly, not this route | Remove or repoint the page to it |
/api/get-icon | Mock/stub (// Mock database or condition) | Remove |
/api/steps/[stepId]/completesteps/check-completion (auto‑complete)/api/steps/record-actionconnected_github)/api/analytics/retention/api/founders/foundersquestionsfounders/foundersquestions.ts instead/api/founders/responses/api/score/api/unppp/priorityunppp/priority-score/api/unppp/effort/api/theunpartybuilder/scores/api/status/api/get-icon// Mock database or condition)_Not listed as orphans (intentionally UI‑less): cron/<em>, webhook/</em>, analytics/vercel,
drains/traces (machine‑triggered); admin/questions/upload, admin/steps/sync-questions,
music/mood-enrich (ops tools); stories (external API‑key feed)._
B. Authorization gaps & inconsistencies
The /api/admin/* namespace mixes three different protection levels:
⛔ No auth at all — admin/steps/[id]/visibility (PATCH & POST) mutates step
visibility with zero authentication. Any anonymous caller can hide/show steps.
⚠️ Sign‑in only, no role check — admin/dashboard-stats, admin/db-stats,
admin/observability, admin/questions, admin/questions/[id],
admin/question-responses, admin/repositories verify auth() has a userId but
never call requireAdmin(). Any signed‑in user can read admin data (DB stats,
observability, every user's responses) or mutate repositories.
🛡️ Correct — the remaining /api/admin/* routes use requireAdmin()
(src/lib/admin.ts → checkRole('admin')).
Recommendation: make requireAdmin() the single gate for the whole /api/admin/*
namespace (ideally enforced in middleware.ts as well as per‑route), so protection can't
drift route‑by‑route.
“Note: the app's
middleware.tsrequires sign‑in for”
“
/admin/<em>pages, but that only forces authentication, not the admin role*, and”
“API routes still need their own server‑side checks.”
C. Legacy, duplicate & stub code
| Item | Where | Note |
|---|---|---|
| Dead route file | src/app/api/book/[bookId]/route-old.ts | Superseded by route.ts; delete |
| Duplicate token logic | music/metadata vs musickit-token | metadata's own comment says it "reuses the same logic"; collapse to one |
| Stub endpoint | get-icon | Comment admits it's a mock; no real store |
| Ephemeral filesystem writes | SVG routes + add-tag | upload-svg, delete-svg, rename-svg, restore-svg, add-tag write under public/, which does not persist on Vercel — moves are lost on redeploy. Migrate to Blob/DB if these must survive |
| In‑memory state | score | See finding A |
src/app/api/book/[bookId]/route-old.tsroute.ts; deletemusic/metadata vs musickit-tokenmetadata's own comment says it "reuses the same logic"; collapse to oneget-iconadd-tagupload-svg, delete-svg, rename-svg, restore-svg, add-tag write under public/, which does not persist on Vercel — moves are lost on redeploy. Migrate to Blob/DB if these must survivescoreD. Structural observations
Unauthenticated writes. The SVG/asset and book/score/chat routes accept
writes with no auth. Acceptable for a solo build, but each is a public write path — gate them (or at least rate‑limit) before launch.
Naming is mostly consistent — verb‑style top‑level SVG routes
(get-svgs, delete-svg, …) sit alongside RESTful nested resources
(steps/[id], admin/…). Fine to leave, but new routes should prefer the nested
RESTful form.
Priority/effort duplication. unppp/priority, unppp/priority-score, and
unppp/effort overlap; consolidating removes a class of "which one is live?" bugs.
---
Maintaining this map
There is no test runner in this repo (see CLAUDE.md); this audit was produced by static inspection. To refresh it after changing routes:
List endpoints & methods — every exported GET/POST/PUT/PATCH/DELETE
handler (as export async function, export function, or export const) under
src/app/api/<em></em>/route.ts(x).
Check wiring — search the repo (outside src/app/api/) for the route path in
fetch(...), href/window.location, <Image>/<img> src, vercel.json crons,
and provider webhook configs. No hit → likely an orphan (finding A).
Check auth — look for requireAdmin(), auth(), STORY_API_KEY, CRON_SECRET,
or a signature verifier at the top of each handler.