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
| File | Role |
|---|---|
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.ts | todos and device_tokens tables |
docs/openapi.yaml | OpenAPI 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:
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
| Concern | Rule |
|---|---|
| Auth | Authorization: Bearer <sessionToken> on every request |
| JSON naming | camelCase |
| Dates | ISO 8601 UTC (2026-07-13T04:42:00.000Z) |
| Success envelope | { "success": true, "data": … } |
| Error envelope | { "success": false, "error": { "code", "message", "requestId" } } |
| Error codes | Stable SCREAMING_SNAKE_CASE enums — branch on code, never parse message |
| Request ID | Send X-Request-Id or let the server generate one; echoed in the response header and error body |
| Idempotency | Non-idempotent writes accept an Idempotency-Key header |
| Versioning | v1 only takes backward-compatible changes; breaking changes ship as v2 alongside |
Error behavior
| HTTP | Code (example) | Trigger |
|---|---|---|
| 400 | INVALID_REQUEST | Zod validation failure, malformed body |
| 401 | UNAUTHENTICATED | Missing, invalid, or revoked session |
| 404 | *_NOT_FOUND | Resource missing or owned by another user |
| 409 | SUBSCRIPTION_ALREADY_ACTIVE | Duplicate purchase while entitled |
| 422 | UNPROCESSABLE | Semantic validation (e.g. unsupported upload type) |
| 429 | RATE_LIMITED | Rate limit hit |
| 500 | INTERNAL | Unexpected 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
| Method | Path | Purpose |
|---|---|---|
| GET / PATCH | /api/app/v1/profile | Read / update the open profile fields |
| GET / POST | /api/app/v1/todos | List / create todos |
| PATCH / DELETE | /api/app/v1/todos/:id | Update / delete one todo |
| POST | /api/app/v1/todos/clear-completed | Delete completed todos |
| PUT | /api/app/v1/todos/reorder | Persist a full ordering |
| PUT / DELETE | /api/app/v1/device-tokens[/:token] | Register / remove an APNs token |
| POST | /api/app/v1/notifications/test-push | Send a test push to the current user's devices |
| GET | /api/app/v1/subscription | Current subscription snapshot + isEntitled |
| POST | /api/app/v1/subscription/portal | Provider-hosted management URL (or null) |
| POST | /api/app/v1/uploads | Request a presigned direct-upload URL |
| POST | /api/app/v1/uploads/complete | Confirm an upload and get its URL |
| DELETE | /api/app/v1/account | Delete 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.
Related pages
Native Authentication
Bearer sessions, email OTP, and Apple/Google ID-token sign-in.
Cross-Platform Subscriptions
One subscription row shared by web and App Store purchases.
Object Storage & Direct Uploads
Presigned uploads to R2 (or local disk in dev).
Push Notifications
Device tokens and the APNs test-push endpoint.
Next.js + Swift Unified Setup
A concrete host-and-client example in about 30 minutes.
