ADR-005: Framework -- Next.js 16 + tRPC v11
Decision record for choosing Next.js 16 with App Router, tRPC v11 for internal APIs, and the five-library state management stack.
Status: Accepted Date: 2026-03-21 (Week 0 Decision Sprint), modified 2026-03-25 (TRO-6/TRO-7 scaffolding) Deciders: Kyle Olson (Solo Founder)
Decision
- Framework: Next.js 16.2 with App Router, React 19, Turbopack
- Language: TypeScript 5.9 strict (
noUncheckedIndexedAccess,exactOptionalPropertyTypes) - Internal API: tRPC v11 -- end-to-end type safety with zero code generation
- Public API: REST via trpc-openapi bridge (deferred to post-traction)
- State stack: Zustand (client), TanStack Query v5 (server/cache), nuqs (URL), React Hook Form (forms), Zod v4 (validation) -- ~32KB total gzipped
- UI: shadcn/ui + Tailwind CSS v4 + Lucide icons
- Output: Standalone mode for Docker containerization
Context
Solo founder building a multi-tenant SaaS with AI coding agents doing the majority of implementation. The framework stack must:
- Optimize for AI agent code generation accuracy -- agents need large training data corpora and predictable patterns
- Provide end-to-end type safety so the TypeScript compiler catches errors before deployment
- Support Server Components as the default rendering mode (simpler mental model, less client JS)
- Run on any platform (no Vercel lock-in) -- currently deployed on a Compute Engine VM
- Support future React Native/Expo mobile app (Phase 2)
Alternatives Considered
Server Actions Only (no tRPC)
Server Actions lack a built-in middleware chain -- every 'use server' function creates a public HTTP endpoint that bypasses middleware, type guards, and component-level protections. Auth, RLS, and CASL checks would need to be manually composed per action, effectively rebuilding tRPC's authorizedProcedure pattern without the structural guarantees. Server Actions also cannot be called from React Native, blocking Phase 2 mobile reuse. Production experience from other SaaS teams (notably Documenso, which migrated to Server Actions and reverted to tRPC within a day) further validated tRPC as the safer choice.
GraphQL (Apollo)
Apollo Studio costs $59-399/month. Schema + resolver + dataloader complexity is error-prone for AI agents. 1864ms vs 922ms REST latency in benchmarks. Requires 3-4 person team to maintain properly. Rejected for solo-dev overhead.
Plain REST
30-40% slower development due to type duplication boilerplate (25-40 lines per endpoint vs tRPC's 5-10). Used only for webhooks and future public API via trpc-openapi bridge.
oRPC v1.0
Released late 2025 -- insufficient AI training data, ecosystem unproven. Deferred evaluation to 2027.
Implementation Notes Relevant to Routing
output: "standalone" for Docker
Set during TRO-7 (CI/CD) for Docker containerization. Produces ~150MB images. Requires pg, pino, pino-pretty in serverExternalPackages. See Docker Containers.
Next.js Pinned to Exact Version
Firebase App Hosting's version checker reads package.json literally, not through pnpm's catalog resolution. Both catalog: and ^16.2.1 failed. Fix: pin "16.2.1" exactly in apps/web/package.json, bypassing the pnpm catalog.
.env Lives in apps/web/
Next.js loads .env from its own project directory, not the monorepo root. Running pnpm dev from root via Turbo silently ignores the root .env. Convention: apps/web/.env (gitignored), with @repo/db scripts using explicit tsx --env-file-if-exists=../../.env.
Extensionless Imports
Turbopack cannot resolve .js-suffixed imports from .ts source files in non-built workspace packages. All relative imports across packages use extensionless paths.
Consequences
Positive
- End-to-end type safety from
@repo/dbthrough@repo/apitoapps/webat compile time, zero codegen - 30-40% faster API development -- tRPC procedures are 5-10 lines vs 25+ for REST
- AI agent accuracy -- dominant ecosystem with massive training data
- Platform-independent -- runs on any Node.js host with standalone output
Negative
- TypeScript pinned to 5.9 (not 6.0) due to
typescript-eslintecosystem lag - shadcn/ui generated files override design tokens and fail lint; requires manual reconciliation
- SSR hydration complexity with React 19 Server Components
Risks
- tRPC language server performance may degrade at 100+ procedures (mitigated by domain-specific router splitting)
- Next.js development heavily influenced by Vercel priorities (mitigated by standalone output, no Vercel-specific features)
Validation
| Rule | Enforcement |
|---|---|
| TypeScript strict mode across all packages | @repo/typescript-config shared config |
tRPC procedures use authorizedProcedure | Architecture test in CI |
No direct SDK imports in apps/web | ESLint no-restricted-imports + dependency-cruiser |
| Standalone output for deployment | next.config.ts sets output: "standalone" |
| Lint + format + typecheck on every PR | CI quality job |
References
- Full ADR:
docs/architecture/decisions/005-framework-nextjs-trpc.md - Architecture: Request Lifecycle
- Related: ADR-004 (Authorization -- procedure chain), ADR-011 (Monorepo -- Turborepo + pnpm)
- Linear: TRO-6, TRO-7, TRO-9, TRO-10, TRO-13