Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0005: API-first architecture; business logic lives in the API, not the client

ADR-0005: API-first architecture; business logic lives in the API, not the client

Status: Accepted Date: ~2026-02 (estimate, post-FIT-17 / WhatsApp planning) Context owner: Owner

Context

The web app is the only shipped client today, but the roadmap commits to:

  • A WhatsApp bot (FIT-17 / FIT-140) that exposes the same actions (booking a class, signing a form, listing schedule) without a browser.
  • A native mobile app (FIT-170 introduces FCM/APNs push; the mobile shell follows).
  • A Spotter agent (FIT-161 shipped) that automates coach workflows via tool calls — the agent is itself a “client” that needs structured access to every action.

If business logic lives in the web client (or worse, is split between client and API), every new client re-implements it and drifts from the others. We’ve seen this fail in adjacent products.

Decision

All business logic lives in the API. The web is a renderer.

  • API responses carry explicit state fields (enums > booleans > inference). Example: a workout response includes status: "draft" | "published" | "archived" rather than letting the client infer it from publishedAt != null.
  • API endpoints expose the full surface needed for any client. The Spotter agent’s tool layer (apps/api/src/ai/agent/tools/) is roughly a 1:1 mirror of the public API surface, calling the same services.
  • Validation lives in Zod schemas under libs/shared/src/lib/schemas/ (shared) and libs/shared/src/lib/agent-schemas/ (Spotter-specific). Both clients and Spotter route through the same validators.
  • The web app is allowed to optimistically update UI and re-fetch, but never to compute derived business state that other clients couldn’t.

Consequences

Positive

  • The WhatsApp bot can be implemented as a thin shell over the API. Same for native, same for Spotter.
  • Tests at the API layer cover all clients simultaneously.
  • Multi-client behavior stays consistent (e.g., a member sees the same “membership expired” message whether they’re on web or get an automated WhatsApp).
  • AI agents (Spotter) compose well — their tools call services, not API HTTP endpoints, and the services are the same code path as HTTP requests.

Negative

  • More API endpoints than a thin-API approach.
  • Some quick UX iterations require an API change (slower than client-only).
  • Aggregation endpoints (returning denormalized “view” responses) proliferate to keep the client fast.

Discipline

  • A web component must never reach into the database directly. The only data access is serverFetch (server components) or useApi (client) — both auto-attach Clerk bearer tokens.
  • If you’re tempted to add a useEffect that fetches three endpoints and joins them in the client: stop, add an aggregation endpoint instead.
  • New API endpoints must accept the same auth model (bearer token, AuthGuard) — no “internal-only” endpoints. The Spotter agent goes through the same auth path.