skills/stack/nextjs-architecture/SKILL.md
# Next.js App Router Architecture Patterns for building production Next.js applications with the App Router. Covers Server/Client Components, data fetching, caching, streaming, PPR, and integration with Supabase and FastAPI. --- ## Server vs Client Components **Default to Server Components.** Every component is a Server Component unless marked with `'use client'`. Server Components produce zero client-side JavaScript. ### When to Use Each | Server Component | Client Component (`'use client
npx skillsauth add 33prime/rtg-forge skills/stack/nextjs-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for building production Next.js applications with the App Router. Covers Server/Client Components, data fetching, caching, streaming, PPR, and integration with Supabase and FastAPI.
Default to Server Components. Every component is a Server Component unless marked with 'use client'. Server Components produce zero client-side JavaScript.
| Server Component | Client Component ('use client') |
|---|---|
| Data fetching, DB queries | useState, useReducer, useEffect |
| Static content, layouts | Event handlers (onClick, onChange) |
| SEO-critical content | Browser APIs (window, localStorage) |
| Everything else by default | Interactive widgets, forms |
Push 'use client' to the smallest leaf components that need interactivity. This is the single biggest performance lever.
// YES — Server page with client island at the leaf
// app/dashboard/page.tsx (Server Component)
import { InteractiveChart } from './InteractiveChart'
export default async function DashboardPage() {
const stats = await getStats() // Runs on server, zero JS
return (
<div>
<h1>Dashboard</h1> {/* Server-rendered, zero JS */}
<StatsSummary stats={stats} /> {/* Server-rendered, zero JS */}
<InteractiveChart data={stats} /> {/* Client island */}
</div>
)
}
// NO — 'use client' on the page makes everything client-side
'use client'
export default function DashboardPage() { /* ... */ }
A Client Component cannot import a Server Component. But you can pass Server Components as children:
// ClientWrapper.tsx
'use client'
export function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true)
return <aside className={open ? 'w-64' : 'w-0'}>{children}</aside>
}
// layout.tsx (Server Component)
<Sidebar>
<ServerNavItems /> {/* Stays server-rendered */}
</Sidebar>
| Pattern | When |
|---------|------|
| Direct call in Server Component | Reading data during page render |
| Server Action ('use server') | Mutations (create, update, delete) |
| Route Handler (route.ts) | Proxying to FastAPI, streaming, webhooks |
Centralize data access in server-only functions that handle auth + queries:
// lib/dal.ts
import 'server-only'
import { createClient } from '@/lib/supabase/server'
export async function getTodos() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')
const { data } = await supabase
.from('todos')
.select('id, title, completed, created_at') // Specific columns, NOT *
return data
}
The server-only import fails the build if accidentally imported into a Client Component.
Always validate inputs. Server Actions are public POST endpoints.
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
const schema = z.object({ title: z.string().min(1).max(200) })
export async function createTodo(formData: FormData) {
const parsed = schema.safeParse({ title: formData.get('title') })
if (!parsed.success) return { error: parsed.error.flatten() }
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')
await supabase.from('todos').insert({ title: parsed.data.title, user_id: user.id })
revalidatePath('/todos')
}
Proxy REST calls to FastAPI through Next.js. Browser never sees FastAPI URL. Zero CORS.
// app/api/v1/[...slug]/route.ts
export async function POST(request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = await params
const url = new URL(slug.join('/'), process.env.FASTAPI_URL)
const supabase = await createClient()
const { data: { session } } = await supabase.auth.getSession()
return fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token}`,
'Content-Type': 'application/json',
},
body: request.body,
duplex: 'half',
})
}
All caching is opt-in since Next.js 15. Use the 'use cache' directive (Next.js 16):
async function getProducts() {
'use cache'
cacheLife('hours')
cacheTag('products')
return await db.query('SELECT * FROM products')
}
// Invalidate after mutation
'use server'
async function updateProduct(id: string, data: ProductUpdate) {
await db.update('products', id, data)
revalidateTag('products')
}
Supabase JS client does NOT use native fetch — Next.js cannot intercept it for caching. Wrap Supabase calls in 'use cache' or use revalidateTag() with Server Actions.
Wrap independent data-fetching components in <Suspense> for progressive rendering:
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1> {/* Instant */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* Streams when ready */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders /> {/* Streams independently */}
</Suspense>
</div>
)
}
<Suspense> close to the data-fetching component, not around the whole pageloading.tsx for route-level fallbacksPromise.all([getA(), getB()]) or sibling Server Componentscache() to deduplicate identical calls across componentsCombines static and dynamic in one route. Static shell from CDN instantly, dynamic holes stream in.
export default function ProductPage() {
return (
<div>
<StaticHeader /> {/* Pre-rendered */}
<StaticProductDetails /> {/* Pre-rendered */}
<Suspense fallback={<Skeleton />}>
<DynamicRecommendations /> {/* Streams in */}
</Suspense>
</div>
)
}
Enable: experimental: { ppr: 'incremental' } in next.config.ts.
lib/supabase/
client.ts — createBrowserClient (Client Components)
server.ts — createServerClient (Server Components, Actions, Route Handlers)
middleware.ts — updateSession() for middleware
Use ONLY getAll() and setAll() cookie methods. Never legacy get/set/remove.
Always getUser(), never getSession() — getSession reads cookies without verifying the JWT.
// middleware.ts
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
// Inside updateSession:
const { data: { user } } = await supabase.auth.getUser() // Verifies JWT
Middleware is a performance optimization (reject early), NOT a security boundary. Also verify in your DAL.
// BAD: N+1
const { data: posts } = await supabase.from('posts').select('*')
// then loop and fetch comments for each...
// GOOD: Resource embedding — single query
const { data: posts } = await supabase
.from('posts')
.select('id, title, comments(id, body, author:profiles(name))')
For LLM streaming, have the client connect to FastAPI directly or proxy through a Route Handler:
// app/api/chat/route.ts — stream-through proxy
export async function POST(req: Request) {
const body = await req.json()
const response = await fetch(`${process.env.FASTAPI_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
return new Response(response.body, {
headers: { 'Content-Type': 'text/event-stream' },
})
}
For long-running streams that may exceed serverless timeouts, connect directly to FastAPI from the client.
| Anti-Pattern | Fix |
|---|---|
| 'use client' on pages or layouts | Push to leaf components |
| fetch('/api/...') in Server Components | Call DAL directly (both run on server) |
| useEffect for data fetching | Fetch in Server Component, pass as props |
| useSearchParams() without Suspense | Wrap in <Suspense> boundary |
| getSession() in middleware | Use getUser() — verifies JWT |
| Assuming fetch() is cached | Explicit 'use cache' or cache: 'force-cache' |
| select('*') with Supabase | Specify columns: select('id, title, status') |
| React Context wrapping Server Components | Pass Server Components as children instead |
| Sole reliance on middleware for auth | Defense in depth: middleware + DAL + Server Action checks |
development
# Parallel Execution > This skill is under development. Workflow patterns for running independent tasks in parallel to improve performance and throughput. ## Topics to Cover - Identifying independent tasks suitable for parallel execution - `asyncio.gather()` with `return_exceptions=True` - `asyncio.TaskGroup` for structured concurrency (Python 3.11+) - Semaphores for bounded concurrency - `Promise.all()` and `Promise.allSettled()` in TypeScript - Handling partial failures (some tasks succeed
development
# Module Extraction > This skill is under development. Workflow for identifying and extracting reusable modules from existing codebases. Extract when a pattern is used in 3+ places and has stabilized. ## Topics to Cover - Identifying extraction candidates (rule of three) - Defining module boundaries and public interface - Dependency analysis: what does the module need? - Interface design: protocols, abstract base classes - Step-by-step extraction process - Testing strategy: tests before, dur
development
# Forge Orchestrate — Intelligent Build Orchestration You are a build planner, not a build executor. Your job is to look at a project, figure out what's left to build, decompose the work into parallel streams, assign the right intelligence level to each stream, estimate cost, and hand the user a set of terminal commands they can run. You plan. They execute. --- ## Stream Decomposition The unit of parallelism is a **stream** — a self-contained bundle of tasks that one Claude session handles e
development
# Code Review > This skill is under development. Workflow for conducting effective code reviews that catch real issues and improve code quality. ## Topics to Cover - Review priorities: correctness > design > performance > style - What to check in every review (checklist) - How to give constructive feedback - Automated checks that should run before human review - Review scope: how big is too big? - Patterns for reviewing database migrations - Patterns for reviewing API changes - When to reque