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
- framework Next.js 15 (App Router), React 18, TypeScript (strict)
- styling Tailwind v4, CSS-first
@themetokens in app/globals.css - data + auth Doable backend via the
doableclient (doable.data, doable.auth) - validation Zod schemas shared between client and server (lib/analysis-schema.ts)
- AI Anthropic API, called only from app/api/analyze/route.ts (server-side)
Pages
| File | Role |
|---|---|
| app/page.tsx | Server component. Renders <Dashboard /> at /. |
| app/profile/page.tsx | Server component. Renders <ProfileForm /> at /profile. |
| app/architecture/page.tsx | This page. Server component, static content. |
API routes
| Method | Path | What it does |
|---|---|---|
| POST /api/analyze | Validates { 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/hello | Scaffold 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
| File | Role |
|---|---|
| components/dashboard.tsx | Owns the stack list, schedule view, and triggers analysis. |
| components/profile-form.tsx | Edits age, sex, weight, height, goals, history. |
| components/stack-entry-sheet.tsx | Add/edit sheet for a single stack entry, backed by the catalog search. |
| components/analysis-panel.tsx | Builds 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
| File | Pattern |
|---|---|
| lib/stack.ts | localStorage ("stak.state.v2") is the source of truth; best-effort doable.data.save("stack", …) mirrors it when configured. |
| lib/profile.ts | localStorage ("stak.profile.v1") is the source of truth; best-effort doable.data.save("profile", …) mirrors it. |
| lib/analysis-cache.ts | Hashes the request (djb2) to skip redundant calls; caches in localStorage ("stak.analysis.v1") and best-effort doable.data.save("analysis", …). |
| lib/catalog.ts | Fetches public/catalog.json (~17k entries) client-side; no Doable involvement. |
| lib/supplements.ts | Local interaction and goal-coverage rules used before the AI analysis runs. |
| lib/api.ts | Thin fetch wrapper for this app's own /api/* routes; throws the server's { error } message on failure. |
| lib/nav.ts | Header nav registry rendered by components/site-header.tsx. |
Request flow: running an analysis
- The dashboard reads the current stack and profile from local state.
analysis-panel.tsxbuilds{ stack, profile }and checkslib/analysis-cache.tsfor a matching cached result.- On a cache miss, it calls
api.post("/analyze", body)fromlib/api.ts. app/api/analyze/route.tsvalidates the body withAnalyzeRequestSchema, rejects empty stacks, and builds a prompt.- The route calls the Anthropic API with
tool_choiceforced tosubmit_analysis, so the model must return structured JSON. - The route validates that JSON with
AnalysisResultSchemabefore returning it; a bad shape becomes a 502, not a leaked stack trace. - The client caches the validated result locally and mirrors it to
doable.data.save("analysis", …)on a best-effort basis.
Notes and gaps
- localStorage is the actual source of truth for stack, profile, and analysis cache. Doable saves are fire-and-forget mirrors; nothing currently reads them back with
doable.data.listor.get, so a second device or browser starts from the seeded defaults. - No automated tests exist yet (CLAUDE.md calls for Vitest + React Testing Library in tests/, not yet set up).