Stak

How this app is built

Architecture

Stak is a single Next.js (App Router) project: pages and API routes live in one codebase, one deploy, no separate backend. This page documents how the pieces fit together.

Stack

Pages

FileRole
app/page.tsxServer component. Renders <Dashboard /> at /.
app/profile/page.tsxServer component. Renders <ProfileForm /> at /profile.
app/architecture/page.tsxThis page. Server component, static content.

API routes

MethodPathWhat it does
POST /api/analyzeValidates { stack, profile } with Zod, calls the Anthropic API server-side (model claude-opus-4-8, tool-forced to submit_analysis), validates the model's output against AnalysisResultSchema, returns the structured result.
GET /api/helloScaffold example route, unused by the app.

The Anthropic API key is read from the server-only env var ANTHROPIC_API_KEY inside app/api/analyze/route.ts and is never sent to the browser.

Client components

FileRole
components/dashboard.tsxOwns the stack list, schedule view, and triggers analysis.
components/profile-form.tsxEdits age, sex, weight, height, goals, history.
components/stack-entry-sheet.tsxAdd/edit sheet for a single stack entry, backed by the catalog search.
components/analysis-panel.tsxBuilds the request body and renders the /api/analyze result (effects, interactions, schedule, safety flags).

Every file above is marked "use client". The pages that render them (app/page.tsx, app/profile/page.tsx) are plain server components.

State and persistence

FilePattern
lib/stack.tslocalStorage ("stak.state.v2") is the source of truth; best-effort doable.data.save("stack", …) mirrors it when configured.
lib/profile.tslocalStorage ("stak.profile.v1") is the source of truth; best-effort doable.data.save("profile", …) mirrors it.
lib/analysis-cache.tsHashes the request (djb2) to skip redundant calls; caches in localStorage ("stak.analysis.v1") and best-effort doable.data.save("analysis", …).
lib/catalog.tsFetches public/catalog.json (~17k entries) client-side; no Doable involvement.
lib/supplements.tsLocal interaction and goal-coverage rules used before the AI analysis runs.
lib/api.tsThin fetch wrapper for this app's own /api/* routes; throws the server's { error } message on failure.
lib/nav.tsHeader nav registry rendered by components/site-header.tsx.

Request flow: running an analysis

  1. The dashboard reads the current stack and profile from local state.
  2. analysis-panel.tsx builds { stack, profile } and checks lib/analysis-cache.ts for a matching cached result.
  3. On a cache miss, it calls api.post("/analyze", body) from lib/api.ts.
  4. app/api/analyze/route.ts validates the body with AnalyzeRequestSchema, rejects empty stacks, and builds a prompt.
  5. The route calls the Anthropic API with tool_choice forced to submit_analysis, so the model must return structured JSON.
  6. The route validates that JSON with AnalysisResultSchema before returning it; a bad shape becomes a 502, not a leaked stack trace.
  7. The client caches the validated result locally and mirrors it to doable.data.save("analysis", …) on a best-effort basis.

Notes and gaps