Skill: ivangdavila/nextjs (version 1.1.0)

--- name: NextJS slug: nextjs version: 1.1.0 homepage: https://clawic.com/skills/nextjs description: Build Next.js 15 apps with App Router, server components, caching, auth, and production patterns. metadata: {"clawdbot":{"emoji":"⚡","requires":{"bins":[]},"os":["linux","darwin","win32"]}} --- ## Setup On first use, read `setup.md` for project integration. ## When to Use User needs Next.js expertise — routing, data fetching, caching, authentication, or deployment. Agent handles App Router patterns, server/client boundaries, and production optimization. ## Architecture Project patterns stored in `~/nextjs/`. See `memory-template.md` for setup. ``` ~/nextjs/ ├── memory.md # Project conventions, patterns └── projects/ # Per-project learnings ``` ## Quick Reference | Topic | File | |-------|------| | Setup | `setup.md` | | Memory template | `memory-template.md` | | Routing (parallel, intercepting) | `routing.md` | | Data fetching & streaming | `data-fetching.md` | | Caching & revalidation | `caching.md` | | Authentication | `auth.md` | | Deployment | `deployment.md` | ## Core Rules ### 1. Server Components by Default Everything is Server Component in App Router. Add `'use client'` only for useState, useEffect, event handlers, or browser APIs. Server Components can't be imported into Client — pass as children. ### 2. Fetch Data on Server Fetch in Server Components, not useEffect. Use `Promise.all` for parallel requests. See `data-fetching.md` for patterns. ### 3. Cache Intentionally `fetch` is cached by default — use `cache: 'no-store'` for dynamic data. Set `revalidate` for ISR. See `caching.md` for strategies. ### 4. Server Actions for Mutations Use `'use server'` functions for form submissions and data mutations. Progressive enhancement — works without JS. See `data-fetching.md`. ### 5. Environment Security `NEXT_PUBLIC_` exposes to client bundle. Server Components access all env vars. Use `.env.local` for secrets. ### 6. Streaming for Large Data Use `<Suspense>` boundaries to stream content progressively. Wrap slow components individually. See `data-fetching.md`. ### 7. Auth at Middleware Level Protect routes in middleware, not in pages. Middleware runs on Edge — lightweight auth checks only. See `auth.md`. ## Server vs Client | Server Component | Client Component | |------------------|------------------| | Default in App Router | Requires `'use client'` | | Can be async | Cannot be async | | Access backend, env vars | Access hooks, browser APIs | | Zero JS shipped | JS shipped to browser | **Decision:** Start Server. Add `'use client'` only for: useState, useEffect, onClick, browser APIs. ## Common Traps | Trap | Fix | |------|-----| | `router.push` in Server | Use `redirect()` | | `<Link>` prefetches all | `prefetch={false}` | | `next/image` no size | Add `width`/`height` or `fill` | | Metadata in Client | Move to Server or `generateMetadata` | | useEffect for data | Fetch in Server Component | | Import Server→Client | Pass as children/props | | Middleware DB call | Call API route instead | | Missing `await params` (v15) | Params are async in Next.js 15 | ## Next.js 15 Changes - `params` and `searchParams` are now `Promise` — must await - `fetch` not cached by default — opt-in with `cache: 'force-cache'` - Use React 19 hooks: `useActionState`, `useFormStatus` ## Related Skills Install with `clawhub install <slug>` if user confirms: - `react` — React fundamentals and patterns - `typescript` — Type safety for better DX - `prisma` — Database ORM for Next.js apps - `tailwindcss` — Styling with utility classes - `nodejs` — Server runtime knowledge ## Feedback - If useful: `clawhub star nextjs` - Stay updated: `clawhub sync`

path: auth.md
content: # Authentication — NextJS

## Auth.js / NextAuth.js Setup

### Installation
```bash
npm install next-auth@beta
```

### Configuration
```typescript
// auth.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import Credentials from 'next-auth/providers/credentials'

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: 'Email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const user = await getUserByEmail(credentials.email)
        if (!user || !verifyPassword(credentials.password, user.password)) {
          return null
        }
        return { id: user.id, email: user.email, name: user.name }
      },
    }),
  ],
  callbacks: {
    authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user
      const isProtected = request.nextUrl.pathname.startsWith('/dashboard')
      if (isProtected && !isLoggedIn) {
        return false // Redirect to login
      }
      return true
    },
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
      }
      return token
    },
    session({ session, token }) {
      session.user.id = token.id as string
      return session
    },
  },
  pages: {
    signIn: '/login',
    error: '/auth/error',
  },
})
```

### Route Handler
```typescript
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers
```

## Middleware Protection

```typescript
// middleware.ts
import { auth } from '@/auth'

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isAuthPage = req.nextUrl.pathname.startsWith('/login')
  const isProtected = req.nextUrl.pathname.startsWith('/dashboard')
  
  // Redirect logged-in users away from auth pages
  if (isAuthPage && isLoggedIn) {
    return Response.redirect(new URL('/dashboard', req.url))
  }
  
  // Redirect unauthenticated users to login
  if (isProtected && !isLoggedIn) {
    const callbackUrl = encodeURIComponent(req.nextUrl.pathname)
    return Response.redirect(new URL(`/login?callbackUrl=${callbackUrl}`, req.url))
  }
})

export const config = {
  matcher: ['/dashboard/:path*', '/login', '/register'],
}
```

## Server Components

```typescript
// Get session in Server Component
import { auth } from '@/auth'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const session = await auth()
  
  if (!session) {
    redirect('/login')
  }
  
  return (
    <div>
      <h1>Welcome, {session.user?.name}</h1>
      <p>Email: {session.user?.email}</p>
    </div>
  )
}
```

## Client Components

```typescript
// app/providers.tsx
'use client'
import { SessionProvider } from 'next-auth/react'

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}

// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

// components/user-nav.tsx
'use client'
import { useSession, signIn, signOut } from 'next-auth/react'

export function UserNav() {
  const { data: session, status } = useSession()
  
  if (status === 'loading') {
    return <div>Loading...</div>
  }
  
  if (!session) {
    return <button onClick={() => signIn()}>Sign In</button>
  }
  
  return (
    <div>
      <span>{session.user?.name}</span>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  )
}
```

## Sign In/Out Forms

```typescript
// components/login-form.tsx
'use client'
import { signIn } from 'next-auth/react'
import { useSearchParams } from 'next/navigation'

export function LoginForm() {
  const searchParams = useSearchParams()
  const callbackUrl = searchParams.get('callbackUrl') || '/dashboard'
  
  return (
    <div>
      <button onClick={() => signIn('github', { callbackUrl })}>
        Sign in with GitHub
      </button>
      <button onClick={() => signIn('google', { callbackUrl })}>
        Sign in with Google
      </button>
      
      {/* Credentials form */}
      <form action={async (formData) => {
        await signIn('credentials', {
          email: formData.get('email'),
          [REDACTED_SECRET]'password'),
          callbackUrl,
        })
      }}>
        <input name="email" type="email" required />
        <input name="password" type="password" required />
        <button type="submit">Sign In</button>
      </form>
    </div>
  )
}
```

## Database Adapter (Prisma)

```typescript
// auth.ts with Prisma adapter
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [...],
  session: {
    strategy: 'jwt', // or 'database' for database sessions
  },
})
```

## Role-Based Access

```typescript
// Extend types
declare module 'next-auth' {
  interface User {
    role: 'admin' | 'user'
  }
  interface Session {
    user: User & { id: string }
  }
}

// Check role in Server Component
export default async function AdminPage() {
  const session = await auth()
  
  if (session?.user?.role !== 'admin') {
    redirect('/unauthorized')
  }
  
  return <AdminDashboard />
}

// Check role in middleware
export default auth((req) => {
  if (req.nextUrl.pathname.startsWith('/admin')) {
    if (req.auth?.user?.role !== 'admin') {
      return Response.redirect(new URL('/unauthorized', req.url))
    }
  }
})
```

## Common Patterns

### Protected API Route
```typescript
// app/api/protected/route.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'

export async function GET() {
  const session = await auth()
  
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  return NextResponse.json({ data: 'Protected data' })
}
```

### Protected Server Action
```typescript
'use server'
import { auth } from '@/auth'

export async function updateProfile(formData: FormData) {
  const session = await auth()
  
  if (!session?.user?.id) {
    throw new Error('Unauthorized')
  }
  
  await db.user.update({
    where: { id: session.user.id },
    data: { name: formData.get('name') },
  })
}
```


path: caching.md
content: # Caching — NextJS

## Cache Types

| Cache | Location | Purpose | Duration |
|-------|----------|---------|----------|
| Request Memoization | Server | Dedupe same fetch in render | Per request |
| Data Cache | Server | Persist fetch results | Indefinite (revalidate) |
| Full Route Cache | Server | Pre-rendered HTML/RSC | Build time |
| Router Cache | Client | RSC payloads | Session/time-based |

## Fetch Caching (Next.js 15)

```typescript
// No cache (default in Next.js 15)
const data = await fetch(url)

// Opt-in to cache
const data = await fetch(url, { cache: 'force-cache' })

// Time-based revalidation
const data = await fetch(url, { next: { revalidate: 60 } })

// Tagged for on-demand revalidation
const data = await fetch(url, { next: { tags: ['posts'] } })
```

## On-Demand Revalidation

```typescript
// In Server Action or Route Handler
import { revalidatePath, revalidateTag } from 'next/cache'

// Revalidate by path
export async function updatePost(id: string, data: any) {
  await db.post.update({ where: { id }, data })
  
  revalidatePath('/blog')           // Specific page
  revalidatePath('/blog/[slug]', 'page')  // Dynamic route
  revalidatePath('/blog', 'layout') // Layout and all pages
  revalidatePath('/', 'layout')     // Entire site
}

// Revalidate by tag
export async function createPost(data: any) {
  await db.post.create({ data })
  revalidateTag('posts')
}

// Route handler for webhook
export async function POST(request: Request) {
  const [REDACTED_SECRET]'x-revalidate-secret')
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }
  
  revalidateTag('posts')
  return Response.json({ revalidated: true })
}
```

## unstable_cache

```typescript
import { unstable_cache } from 'next/cache'

// Cache database queries
const getCachedUser = unstable_cache(
  async (id: string) => {
    return db.user.findUnique({ where: { id } })
  },
  ['user'],
  {
    tags: ['users'],
    revalidate: 60,
  }
)

// Usage
const user = await getCachedUser('123')

// Revalidate
revalidateTag('users')
```

## Route Segment Config

```typescript
// Force dynamic rendering (no cache)
export const dynamic = 'force-dynamic'

// Force static rendering
export const dynamic = 'force-static'

// Set revalidation time
export const revalidate = 60 // seconds

// Fetch cache behavior
export const fetchCache = 'force-cache'      // Cache all fetches
export const fetchCache = 'force-no-store'   // No cache

// Combined example
export const dynamic = 'force-dynamic'
export const revalidate = 0
```

## Router Cache (Client)

```typescript
// Automatic - Link prefetching
<Link href="/about">About</Link>

// Disable prefetch
<Link href="/about" prefetch={false}>About</Link>

// Manual invalidation
import { useRouter } from 'next/navigation'

function Component() {
  const router = useRouter()
  
  // Force refresh from server
  router.refresh()
}
```

## ISR (Incremental Static Regeneration)

```typescript
// Page-level revalidation
export const revalidate = 60 // seconds

// Or fetch-level
async function Page() {
  const data = await fetch(url, {
    next: { revalidate: 60 }
  })
}

// Shortest revalidate wins for the page
// fetch with 30s + fetch with 60s = page revalidates at 30s
```

## Static Generation with Dynamic Params

```typescript
// Generate static pages at build time
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map(post => ({ slug: post.slug }))
}

// Control behavior for unlisted params
export const dynamicParams = true   // Generate on-demand (default)
export const dynamicParams = false  // Return 404

// Force static even with dynamic data
export const dynamic = 'force-static'
```

## Cache Debugging

```typescript
// Log cache status
async function Page() {
  const res = await fetch(url)
  console.log('Cache:', res.headers.get('x-vercel-cache')) // On Vercel
  console.log('Age:', res.headers.get('age'))
}

// Development: caching is disabled by default
// Use `next build && next start` to test production caching

// Headers to check
// x-nextjs-cache: HIT | MISS | STALE
// x-vercel-cache: HIT | MISS | STALE | PRERENDER
```

## Common Patterns

### Cache with Dynamic User Data
```typescript
// Static page shell + dynamic user data
async function Page() {
  return (
    <>
      <StaticContent />
      <Suspense fallback={<Skeleton />}>
        <DynamicUserData />
      </Suspense>
    </>
  )
}

async function DynamicUserData() {
  const session = await auth()
  // Dynamic - not cached
  const userData = await getUserData(session.user.id)
  return <UserWidget data={userData} />
}
```

### Cache Everything Except Auth
```typescript
// Cache the page
export const revalidate = 3600 // 1 hour

async function Page() {
  const posts = await getCachedPosts() // Cached
  
  return (
    <>
      <PostList posts={posts} />
      {/* Auth-aware component - client-side */}
      <AuthActions />
    </>
  )
}

'use client'
function AuthActions() {
  const { data: session } = useSession()
  if (!session) return <LoginButton />
  return <CreatePostButton />
}
```

### Stale-While-Revalidate
```typescript
// Show stale content immediately, revalidate in background
export const revalidate = 60

async function Page() {
  // First request: generate and cache
  // Within 60s: serve from cache
  // After 60s: serve stale, regenerate in background
  const data = await fetch(url, {
    next: { revalidate: 60 }
  })
}
```


path: data-fetching.md
content: # Data Fetching — NextJS

## Server Components (Recommended)

```typescript
// Direct fetch in Server Component
async function Page() {
  const data = await fetch('https://api.example.com/data')
  const json = await data.json()
  return <div>{json.title}</div>
}

// With database
async function Page() {
  const posts = await db.post.findMany()
  return <PostList posts={posts} />
}
```

## Parallel Data Fetching

```typescript
// ✅ Good - parallel fetches
async function Dashboard() {
  // Start both fetches simultaneously
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ])
  
  return (
    <>
      <UserCard user={user} />
      <PostList posts={posts} />
      <CommentList comments={comments} />
    </>
  )
}

// ❌ Bad - sequential waterfall
async function Dashboard() {
  const user = await getUser()      // waits
  const posts = await getPosts()    // then waits
  const comments = await getComments() // then waits
  // Total time = sum of all fetches
}
```

## Streaming with Suspense

```typescript
import { Suspense } from 'react'

export default function Page() {
  return (
    <main>
      {/* Shows immediately */}
      <h1>Dashboard</h1>
      
      {/* Streams when ready */}
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile />
      </Suspense>
      
      {/* Independent stream */}
      <Suspense fallback={<PostsSkeleton />}>
        <RecentPosts />
      </Suspense>
    </main>
  )
}

// Each component fetches its own data
async function UserProfile() {
  const user = await getUser() // 200ms
  return <div>{user.name}</div>
}

async function RecentPosts() {
  const posts = await getPosts() // 500ms
  return <PostList posts={posts} />
}
// User shows at 200ms, Posts at 500ms
```

## Loading UI

```typescript
// app/dashboard/loading.tsx
// Automatically wraps page in Suspense

export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 w-48 bg-gray-200 rounded" />
      <div className="mt-4 space-y-3">
        <div className="h-4 bg-gray-200 rounded" />
        <div className="h-4 bg-gray-200 rounded" />
      </div>
    </div>
  )
}
```

## Error Handling

```typescript
// app/dashboard/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}
```

## Fetch Options

```typescript
// Cached (default in Next.js 14, opt-in in 15)
fetch(url, { cache: 'force-cache' })

// No cache
fetch(url, { cache: 'no-store' })

// Time-based revalidation (ISR)
fetch(url, { next: { revalidate: 60 } }) // seconds

// Tag-based revalidation
fetch(url, { next: { tags: ['posts'] } })

// Revalidate by tag (Server Action)
import { revalidateTag } from 'next/cache'
revalidateTag('posts')
```

## Request Deduplication

```typescript
// Same fetch in multiple components = 1 request
async function Layout({ children }) {
  const user = await getUser() // Request 1
  return <>{children}</>
}

async function Page() {
  const user = await getUser() // Deduped, uses Request 1
  return <div>{user.name}</div>
}

// Works for fetch() with same URL and options
// Also works with React cache()
import { cache } from 'react'

export const getUser = cache(async () => {
  const res = await fetch('/api/user')
  return res.json()
})
```

## Client-Side Fetching (When Needed)

```typescript
'use client'
import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

export function UserPosts() {
  const { data, error, isLoading } = useSWR('/api/posts', fetcher)
  
  if (isLoading) return <Skeleton />
  if (error) return <Error />
  return <PostList posts={data} />
}

// With React Query
'use client'
import { useQuery } from '@tanstack/react-query'

export function UserPosts() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(r => r.json()),
  })
  
  if (isLoading) return <Skeleton />
  if (error) return <Error />
  return <PostList posts={data} />
}
```

## Preloading Data

```typescript
import { preload } from 'react-dom'

// Preload in parent, use in child
export default function Page() {
  preload('/api/user', { as: 'fetch' })
  return <UserProfile />
}

// With custom function
import { unstable_preload as preload } from 'next/cache'

export const preloadUser = (id: string) => {
  void getUser(id)
}

// Parent preloads
export default function Layout({ children }) {
  preloadUser('123')
  return <>{children}</>
}
```

## Search Params

```typescript
// Server Component - use searchParams prop
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; page?: string }>
}) {
  const { q, page } = await searchParams
  const results = await search(q, parseInt(page || '1'))
  return <Results data={results} />
}

// Client Component - use useSearchParams
'use client'
import { useSearchParams, useRouter } from 'next/navigation'

export function SearchInput() {
  const searchParams = useSearchParams()
  const router = useRouter()
  const q = searchParams.get('q') || ''
  
  function handleSearch(term: string) {
    const params = new URLSearchParams(searchParams)
    if (term) {
      params.set('q', term)
    } else {
      params.delete('q')
    }
    router.push(`?${params.toString()}`)
  }
  
  return <input defaultValue={q} onChange={e => handleSearch(e.target.value)} />
}
```

## Patterns

### Fetch on Server, Hydrate on Client
```typescript
// Fetch on server
async function Page() {
  const initialData = await getPosts()
  return <PostsClient initialData={initialData} />
}

// Hydrate on client for real-time updates
'use client'
function PostsClient({ initialData }) {
  const { data } = useSWR('/api/posts', fetcher, {
    fallbackData: initialData,
  })
  return <PostList posts={data} />
}
```

### Infinite Scroll
```typescript
'use client'
import useSWRInfinite from 'swr/infinite'

const getKey = (pageIndex: number, previousPageData: any) => {
  if (previousPageData && !previousPageData.length) return null
  return `/api/posts?page=${pageIndex + 1}`
}

export function InfiniteList() {
  const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher)
  
  const posts = data ? data.flat() : []
  const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined')
  const isEmpty = data?.[0]?.length === 0
  const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10)
  
  return (
    <>
      {posts.map(post => <Post key={post.id} post={post} />)}
      <button
        disabled={isLoadingMore || isReachingEnd}
        onClick={() => setSize(size + 1)}
      >
        {isLoadingMore ? 'Loading...' : isReachingEnd ? 'No more' : 'Load more'}
      </button>
    </>
  )
}
```


path: deployment.md
content: # Deployment — NextJS

## Build & Output

```bash
# Production build
npm run build

# Output modes in next.config.js
module.exports = {
  output: 'standalone',  // Self-contained Node.js server
  // output: 'export',   // Static HTML export (no server features)
}
```

## Vercel (Recommended)

```bash
# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

# Production deploy
vercel --prod
```

**vercel.json:**
```json
{
  "buildCommand": "npm run build",
  "outputDirectory": ".next",
  "framework": "nextjs",
  "regions": ["iad1"],
  "env": {
    "DATABASE_URL": "@database-url"
  }
}
```

## Standalone Output

```javascript
// next.config.js
module.exports = {
  output: 'standalone',
}
```

```bash
# Build
npm run build

# Copy static files (REQUIRED)
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public

# Run
node .next/standalone/server.js
# Listens on port 3000
```

## Docker

**Dockerfile:**
```dockerfile
FROM node:20-alpine AS base

# Dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]
```

**.dockerignore:**
```
node_modules
.next
.git
*.md
```

```bash
# Build
docker build -t nextjs-app .

# Run
docker run -p 3000:3000 nextjs-app
```

## Docker Compose

```yaml
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    depends_on:
      - db
    restart: unless-stopped
  
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb

volumes:
  postgres_data:
```

## Environment Variables

```bash
# .env.local (development, gitignored)
DATABASE_URL=postgresql://localhost:5432/mydb
NEXTAUTH_SECRET=your-secret

# .env.production (production defaults, committed)
NEXT_PUBLIC_SITE_URL=https://example.com

# Runtime env (override at deploy)
DATABASE_URL=postgresql://prod:5432/mydb
```

**Loading order:**
1. `.env.local` (not in test)
2. `.env.[environment].local`
3. `.env.[environment]`
4. `.env`

**Access:**
```typescript
// Server only (no prefix)
process.env.DATABASE_URL

// Client + Server (with prefix)
process.env.NEXT_PUBLIC_SITE_URL
```

## Static Export

```javascript
// next.config.js
module.exports = {
  output: 'export',
  // Optional: custom output directory
  distDir: 'dist',
  // Optional: trailing slashes
  trailingSlash: true,
}
```

**Limitations:**
- No Server Components (all become static)
- No API Routes
- No ISR/Revalidation
- No Middleware
- No Image Optimization (use external loader)

```bash
npm run build
# Output in /out folder
# Deploy to any static host (Netlify, GitHub Pages, S3)
```

## Nginx Reverse Proxy

```nginx
# /etc/nginx/sites-available/nextjs
server {
    listen 80;
    server_name example.com;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
```

## PM2 Process Manager

```javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: '.next/standalone/server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
  }],
}
```

```bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup
```

## Health Checks

```typescript
// app/api/health/route.ts
export async function GET() {
  try {
    // Check database
    await db.$queryRaw`SELECT 1`
    
    return Response.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
    })
  } catch (error) {
    return Response.json(
      { status: 'unhealthy', error: error.message },
      { status: 503 }
    )
  }
}
```

## Performance

```javascript
// next.config.js
module.exports = {
  // Compress responses
  compress: true,
  
  // Optimize images
  images: {
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 60 * 60 * 24 * 365, // 1 year
  },
  
  // Bundle analyzer
  webpack: (config, { isServer }) => {
    if (process.env.ANALYZE === 'true') {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
      config.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: isServer ? 'server.html' : 'client.html',
        })
      )
    }
    return config
  },
}
```

## Monitoring

```typescript
// instrumentation.ts (Next.js 15)
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    // OpenTelemetry, Sentry, etc.
    await import('./sentry.server.config')
  }
}

// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
})
```

## CI/CD Example (GitHub Actions)

```yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      
      - run: npm ci
      - run: npm run build
      - run: npm test
      
      - uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'
```


path: memory-template.md
content: # Memory Template — NextJS

## ~/nextjs/memory.md

```markdown
# Next.js Patterns

## Stack
- **Version:** 
- **Router:** App Router / Pages Router
- **TypeScript:** Yes / No
- **Database:** 
- **Auth:** 
- **Styling:** 
- **Deployment:** 

## Preferences
- **Proactive help:** Yes / On request only
- **Component naming:** 
- **File organization:** 

## Project Conventions
<!-- Add team/personal conventions here -->

## Learned Patterns
<!-- Patterns discovered through working together -->
```

## ~/nextjs/projects/{name}.md

```markdown
# {Project Name}

## Overview
- **Type:** 
- **Status:** 
- **Repo:** 

## Architecture Decisions
<!-- Key decisions and why -->

## Caching Strategy
<!-- ISR, on-demand, static -->

## Gotchas
<!-- Project-specific traps to remember -->
```

## Status Fields

| Field | Values |
|-------|--------|
| Version | 13 / 14 / 15 |
| Router | App Router / Pages Router |
| TypeScript | Yes / No |
| Proactive help | Yes / On request only |


path: routing.md
content: # Routing — NextJS

## File-Based Routing

```
app/
├── page.tsx                    # /
├── about/
│   └── page.tsx               # /about
├── blog/
│   ├── page.tsx               # /blog
│   └── [slug]/
│       └── page.tsx           # /blog/hello-world
└── [...catchAll]/
    └── page.tsx               # /anything/else/here
```

## Special Files

| File | Purpose |
|------|---------|
| `page.tsx` | Unique UI for route, makes route accessible |
| `layout.tsx` | Shared UI, wraps pages and nested layouts |
| `loading.tsx` | Loading UI (Suspense boundary) |
| `error.tsx` | Error UI (Error boundary) |
| `not-found.tsx` | 404 UI for route |
| `template.tsx` | Like layout but re-mounts on navigation |
| `default.tsx` | Fallback for parallel routes |
| `route.tsx` | API endpoint |

## Dynamic Routes

```typescript
// app/blog/[slug]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return <h1>Post: {slug}</h1>
}

// Generate static params
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

// Control dynamic behavior
export const dynamicParams = true  // Allow unlisted params (default)
export const dynamicParams = false // 404 for unlisted params
```

## Catch-All Routes

```typescript
// app/shop/[...categories]/page.tsx
// Matches: /shop/clothes, /shop/clothes/tops, /shop/clothes/tops/shirts

export default async function Page({
  params,
}: {
  params: Promise<{ categories: string[] }>
}) {
  const { categories } = await params
  // categories = ['clothes', 'tops', 'shirts']
  return <h1>{categories.join(' > ')}</h1>
}

// Optional catch-all: [[...categories]]
// Also matches: /shop (categories = undefined)
```

## Route Groups

```
app/
├── (marketing)/          # Group - no URL impact
│   ├── page.tsx         # /
│   ├── about/page.tsx   # /about
│   └── layout.tsx       # Marketing layout
├── (shop)/
│   ├── products/page.tsx # /products
│   └── layout.tsx       # Shop layout
└── layout.tsx           # Root layout
```

## Parallel Routes

```
app/
├── @analytics/
│   ├── page.tsx
│   └── default.tsx      # Fallback when no match
├── @team/
│   ├── page.tsx
│   └── default.tsx
├── layout.tsx           # Receives slots as props
└── page.tsx
```

```typescript
// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      <aside>
        {analytics}
        {team}
      </aside>
    </>
  )
}
```

## Intercepting Routes

| Convention | Matches |
|------------|---------|
| `(.)folder` | Same level |
| `(..)folder` | One level up |
| `(..)(..)folder` | Two levels up |
| `(...)folder` | From root |

```
app/
├── feed/
│   └── page.tsx
├── photo/[id]/
│   └── page.tsx          # Full page /photo/123
└── @modal/
    └── (.)photo/[id]/
        └── page.tsx      # Intercepts as modal
```

## Navigation

```typescript
// Link component - client-side navigation
import Link from 'next/link'

<Link href="/about">About</Link>
<Link href={`/blog/${post.slug}`}>Read More</Link>
<Link href="/dashboard" prefetch={false}>Dashboard</Link>

// useRouter - programmatic navigation (Client Component)
'use client'
import { useRouter } from 'next/navigation'

function Component() {
  const router = useRouter()
  
  router.push('/dashboard')        // Navigate
  router.replace('/login')         // Replace history
  router.refresh()                 // Refresh server components
  router.prefetch('/about')        // Prefetch route
  router.back()                    // Go back
  router.forward()                 // Go forward
}

// redirect - Server Component/Action
import { redirect } from 'next/navigation'

export default async function Page() {
  const session = await getSession()
  if (!session) redirect('/login')
  return <Dashboard />
}

// permanentRedirect - 308 redirect
import { permanentRedirect } from 'next/navigation'
permanentRedirect('/new-url')
```

## Route Configuration

```typescript
// Per-route configuration
export const dynamic = 'auto' // default
export const dynamic = 'force-dynamic' // always dynamic
export const dynamic = 'force-static' // always static
export const dynamic = 'error' // error if dynamic

export const revalidate = false // no revalidation (default for static)
export const revalidate = 0 // always revalidate
export const revalidate = 60 // revalidate every 60 seconds

export const fetchCache = 'auto' // default
export const fetchCache = 'default-cache' // cache unless opt-out
export const fetchCache = 'default-no-store' // no cache unless opt-in
export const fetchCache = 'force-cache' // cache all
export const fetchCache = 'force-no-store' // no cache
export const fetchCache = 'only-cache' // error on no-cache
export const fetchCache = 'only-no-store' // error on cache

export const runtime = 'nodejs' // default
export const runtime = 'edge' // edge runtime

export const preferredRegion = 'auto' // default
export const preferredRegion = 'iad1' // specific region
export const preferredRegion = ['iad1', 'sfo1'] // multiple regions

export const maxDuration = 5 // max execution time in seconds
```

## Active Link

```typescript
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

export function NavLinks() {
  const pathname = usePathname()
  
  const links = [
    { href: '/', label: 'Home' },
    { href: '/about', label: 'About' },
    { href: '/blog', label: 'Blog' },
  ]
  
  return (
    <nav>
      {links.map((link) => (
        <Link
          key={link.href}
          href={link.href}
          className={pathname === link.href ? 'active' : ''}
        >
          {link.label}
        </Link>
      ))}
    </nav>
  )
}
```


path: setup.md
content: # Setup — NextJS

Read this when no `~/nextjs/memory.md` exists. Start naturally — don't mention setup.

## Your Attitude

You're a Next.js expert who's helped ship hundreds of production apps. You're practical, not theoretical — focus on patterns that work in real codebases. Get excited about helping them build something solid.

## Priority Order

### 1. First: Integration

Within the first few exchanges, understand how they want Next.js help:
- "Want me to jump in whenever you're working on Next.js, or only when you ask?"
- "Should I flag potential issues proactively (caching gotchas, performance), or just help when asked?"
- "Any specific patterns or conventions you want me to enforce?"

Save this to their main memory so other sessions know when to activate.

### 2. Then: Understand Their Project

Get the big picture:
- What are you building? (SaaS, e-commerce, blog, dashboard)
- Which Next.js version? (13/14/15, App Router or Pages?)
- What's your stack? (TypeScript? Prisma? Auth?)
- Deployment target? (Vercel, self-hosted, Docker)

### 3. Finally: Conventions (if they want)

Some teams have strong opinions:
- Component naming (`PascalCase.tsx` vs `kebab-case.tsx`)
- File organization (feature folders vs type folders)
- State management (none, Zustand, context)
- Styling (Tailwind, CSS modules, styled-components)

Only dig into these if they mention conventions matter to them.

## Feedback After Each Response

When they share project info, connect it to how you'll help:
- "Next.js 15 with App Router and Prisma — nice stack. I'll keep an eye on server/client boundaries and help you avoid the Prisma gotchas with Server Components."
- "E-commerce with heavy caching — got it. I'll flag when ISR makes sense vs on-demand revalidation."

## What You're Saving (internally)

After learning about their setup, save to `~/nextjs/memory.md`:
- Next.js version and router type
- Key dependencies (Prisma, Auth.js, etc.)
- Deployment target
- Any explicit conventions they mentioned
- How proactive they want you to be

For project-specific patterns, use `~/nextjs/projects/{name}.md`.

## When Done

Once you know their stack and how they want help, you're ready. Everything else builds naturally through working together.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)