App API Overview

The versioned /api/app/v1 backend that native clients share with the web app.

The template ships a versioned App API (/api/app/v1) — a full-featured HTTP backend that Swift, Kotlin, Expo, Electron, and Tauri clients consume directly. Next.js, Nuxt, and TanStack Start implement the same public contract. Web pages and clients share one Better Auth user, one database, and one subscription: sign in on iOS with the account you created on the web, read the same data, and unlock the same entitlement from either channel.

What it provides

  • 15 business endpoints under /api/app/v1: profile, todos, device tokens, APNs test push, subscription snapshot + management portal, presigned uploads, and account deletion.
  • A service layer (src/server/app/) that route handlers and web pages both call — business logic is written once.
  • Bearer-session auth on every endpoint; ownership always comes from the server session, never from a client-supplied user ID.
  • A machine-readable OpenAPI 3.1 contract at docs/openapi.yaml.

Authentication itself is not re-invented inside /api/app/v1. Native clients use the same Better Auth endpoints under /api/auth/* with a Bearer token — see Native Authentication.

Important files

FileRole
src/app/api/app/v1/Route handlers (HTTP boundary only)
src/server/app/shared/Session guard, response envelopes, request IDs, logging, rate limits, idempotency
src/server/app/{profile,todos,devices,push,subscriptions,uploads,account}/Business services
src/lib/db/schema/app.tstodos and device_tokens tables
docs/openapi.yamlOpenAPI 3.1 contract — the single source of truth

Architecture

Every route is a thin wrapper: authedRoute builds a request context, enforces a valid session, and maps thrown AppErrors to uniform error envelopes. The handler delegates to a service that receives the server-verified user ID:

src/app/api/app/v1/todos/route.ts (shape)
export const GET = authedRoute(async (ctx) => {
  const items = await todosService.list(ctx.userId, filter);
  return ok({ items }, ctx);
});

Services never import NextRequest/NextResponse, so the same functions back the web dashboard and the App API.

API conventions

ConcernRule
AuthAuthorization: Bearer <sessionToken> on every request
JSON namingcamelCase
DatesISO 8601 UTC (2026-07-13T04:42:00.000Z)
Success envelope{ "success": true, "data": … }
Error envelope{ "success": false, "error": { "code", "message", "requestId" } }
Error codesStable SCREAMING_SNAKE_CASE enums — branch on code, never parse message
Request IDSend X-Request-Id or let the server generate one; echoed in the response header and error body
IdempotencyNon-idempotent writes accept an Idempotency-Key header
Versioningv1 only takes backward-compatible changes; breaking changes ship as v2 alongside

Error behavior

HTTPCode (example)Trigger
400INVALID_REQUESTZod validation failure, malformed body
401UNAUTHENTICATEDMissing, invalid, or revoked session
404*_NOT_FOUNDResource missing or owned by another user
409SUBSCRIPTION_ALREADY_ACTIVEDuplicate purchase while entitled
422UNPROCESSABLESemantic validation (e.g. unsupported upload type)
429RATE_LIMITEDRate limit hit
500INTERNALUnexpected error, details never leaked

Cross-user access returns 404, not 403. Every resource query filters by the resource ID and the session's user ID, so the API never reveals whether another user's resource exists.

Endpoints

MethodPathPurpose
GET / PATCH/api/app/v1/profileRead / update the open profile fields
GET / POST/api/app/v1/todosList / create todos
PATCH / DELETE/api/app/v1/todos/:idUpdate / delete one todo
POST/api/app/v1/todos/clear-completedDelete completed todos
PUT/api/app/v1/todos/reorderPersist a full ordering
PUT / DELETE/api/app/v1/device-tokens[/:token]Register / remove an APNs token
POST/api/app/v1/notifications/test-pushSend a test push to the current user's devices
GET/api/app/v1/subscriptionCurrent subscription snapshot + isEntitled
POST/api/app/v1/subscription/portalProvider-hosted management URL (or null)
POST/api/app/v1/uploadsRequest a presigned direct-upload URL
POST/api/app/v1/uploads/completeConfirm an upload and get its URL
DELETE/api/app/v1/accountDelete the account and cascade owned data

PUT /api/app/v1/uploads/local also exists as a development-only target for the local storage provider; it is disabled in production and not part of the contract.

The OpenAPI contract

docs/openapi.yaml describes every endpoint, schema, enum, and error response. It is the single source of truth for client implementations — point any OpenAPI viewer or generator at it, and validate hand-written client models against it in contract tests.

On this page