Imported from Jicate-Solutions/arts_tally_roja (
.claude/skills/nextjs16-web-development/SKILL.md). Install upstream withnpx skills add Jicate-Solutions/arts_tally_roja --skill nextjs16-web-development. Copyright stays with the author.
Next.js 16 Web Development
Complete production-ready workflow for building modern Next.js 16 applications with optimal performance, security, and developer experience.
🚨 CRITICAL: Read Error Prevention Guide First
BEFORE implementing ANY features, read the Error Prevention Guide to avoid the THREE MOST COMMON ERRORS that occur repeatedly in Next.js 16:
- Uncached data accessed outside of - Blocks entire page render
- cookies()/headers() inside 'use cache' - Architectural violation
- searchParams accessed without await - Promise must be unwrapped
These are NOT bugs - they're architectural requirements. The Error Prevention Guide shows exactly what NOT to do and the correct patterns to use.
Core Paradigm Shift
Next.js 15: Static by default → opt into dynamic
Next.js 16: Dynamic by default → opt into caching with use cache
This fundamental shift provides fine-grained control over caching at the function level rather than page level.
When to Use This Skill
Use this skill when:
- Starting a new Next.js 16 project with proper configuration and structure
- Building CRUD modules with optimal caching and Server Actions
- Implementing forms with validation, error handling, and optimistic updates
- Optimizing performance with Cache Components and PPR
- Designing database schemas with Supabase and Row Level Security
- Migrating from Next.js 15 to Next.js 16 architecture
- Standardizing team workflows for consistent Next.js 16 development
- Creating real-time features with appropriate cache strategies
Quick Decision Framework
Caching Strategy
Is the data user-specific?
├─ YES → Is it personalized but cacheable?
│ ├─ YES → 'use cache: private' + appropriate cacheLife
│ └─ NO → Don't cache (use runtime APIs like cookies)
└─ NO → How often does it change?
├─ Real-time → No cache
├─ Seconds → 'use cache' + cacheLife('realtime' or 'seconds')
├─ Minutes → 'use cache' + cacheLife('frequent' or 'minutes')
├─ Hours → 'use cache' + cacheLife('moderate' or 'hours')
├─ Days → 'use cache' + cacheLife('days')
└─ Rarely → 'use cache' + cacheLife('weeks' or 'static')
Cache Invalidation Strategy
User expects instant update?
├─ YES → updateTag() - Instant invalidation
└─ NO → revalidateTag() - Background refresh
- updateTag: Use for INSTANT updates when users expect immediate feedback (save, delete, update)
- revalidateTag: Use for BACKGROUND updates where slight delays are acceptable (periodic syncs)
Server Actions vs Route Handlers
Need to handle form submission?
├─ YES → Server Action
└─ NO → Building API for external use?
├─ YES → Route Handler
└─ NO → Server Action
Use Server Actions for:
- Form submissions
- CRUD operations (create/update/delete)
- Mutations with CSRF protection
- Progressive enhancement
Use Route Handlers for:
- External webhooks
- REST/GraphQL APIs for external consumers
- File downloads
- Specific HTTP method requirements
Project Initialization Workflow
1. Create Next.js 16 Project
# Initialize with Turbopack
npx create-next-app@latest project-name --typescript --tailwind --app --turbopack
# Install core dependencies
npm install @supabase/supabase-js @supabase/ssr
npm install zod react-hook-form @hookform/resolvers
npm install date-fns clsx tailwind-merge lucide-react
2. Configure Next.js 16 (Critical)
Enable Cache Components in next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true, // Enable PPR and use cache
cacheLife: {
// Predefined profiles with stale, revalidate, expire
default: {
stale: 300, // 5 minutes - Client considers data stale
revalidate: 900, // 15 minutes - Server background refresh
expire: 3600 // 1 hour - Hard expiration
},
// Production-grade cache profiles (based on MyJKKN patterns)
hot: {
stale: 60, // 1 minute - Real-time data (payments, sessions)
revalidate: 300, // 5 minutes - Background refresh
expire: 600 // 10 minutes - Hard expiration
},
warm: {
stale: 300, // 5 minutes - Moderate freshness (invoices, profiles)
revalidate: 900, // 15 minutes - Background refresh
expire: 1800 // 30 minutes - Hard expiration
},
cold: {
stale: 3600, // 1 hour - Master data (institutions, departments)
revalidate: 7200, // 2 hours - Background refresh
expire: 14400 // 4 hours - Hard expiration
},
static: {
stale: 86400, // 1 day - Configuration data
revalidate: 172800, // 2 days - Background refresh
expire: 604800 // 7 days - Hard expiration
},
// Short-hand profiles (for simple cases)
seconds: { stale: 5, revalidate: 15, expire: 30 },
minutes: { stale: 60, revalidate: 180, expire: 300 },
hours: { stale: 3600, revalidate: 7200, expire: 14400 },
days: { stale: 86400, revalidate: 172800, expire: 604800 },
weeks: { stale: 604800, revalidate: 1209600, expire: 2592000 },
max: {
stale: Number.MAX_SAFE_INTEGER,
revalidate: Number.MAX_SAFE_INTEGER,
expire: Number.MAX_SAFE_INTEGER
},
},
}
export default nextConfig
Understanding cacheLife parameters:
- stale: Client-side cache duration (minimum 30s enforced by Next.js)
- revalidate: Server-side background refresh interval
- expire: Hard expiration - data MUST be refetched after this time
3. Standard Project Structure
app/
├── (auth)/ # Auth routes group
├── (dashboard)/ # Protected routes group
├── actions/ # Server Actions by module
├── api/ # API routes (webhooks only)
lib/
├── supabase/ # Supabase clients (server.ts, client.ts)
├── data/ # Cached data fetching functions
├── validations/ # Zod schemas
├── utils/ # Utilities (cn.ts, format.ts)
components/
├── ui/ # Shadcn/UI components
├── shared/ # Shared components
├── forms/ # Form components
types/ # TypeScript types
config/ # App configuration
Core Patterns
Pattern 0: Deferring to Request Time with connection()
For non-deterministic operations that should NOT be cached (random values, timestamps, UUIDs):
import { connection } from 'next/server'
export default async function Page() {
// Explicitly defer to request time
await connection()
// Now safe to use non-deterministic operations
const random = Math.random()
const timestamp = Date.now()
const uuid = crypto.randomUUID()
const randomBytes = crypto.getRandomValues(new Uint8Array(16))
return (
<div>
<p>Random: {random}</p>
<p>Time: {timestamp}</p>
<p>UUID: {uuid}</p>
</div>
)
}
When to use connection():
- Math.random() or crypto.getRandomValues()
- Date.now() or new Date()
- crypto.randomUUID()
- Any operation that should produce different results per request
- When you need dynamic rendering without accessing cookies/headers
Note: connection() replaces unstable_noStore from Next.js 14/15.
Pattern 1: Cached Data Fetching
// lib/data/products.ts
import { cacheTag, cacheLife } from 'next/cache'
import { createServerSupabaseClient } from '@/lib/supabase/server'
export async function getProducts(filters?: ProductFilters) {
'use cache'
cacheLife('hours') // or 'minutes', 'days', 'weeks'
cacheTag('products') // for invalidation
const supabase = await createServerSupabaseClient()
let query = supabase
.from('products')
.select('*, category:categories(name)', { count: 'exact' })
// Apply filters
if (filters?.search) {
query = query.textSearch('name', filters.search)
}
const { data, error, count } = await query
if (error) throw error
return { data: data || [], total: count || 0 }
}
Pattern 2: Server Actions for Mutations
// app/actions/products.ts
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { createServerSupabaseClient } from '@/lib/supabase/server'
import { CreateProductSchema } from '@/types/product'
export async function createProduct(
prevState: FormState,
formData: FormData
): Promise<FormState> {
// Validate input
const validation = CreateProductSchema.safeParse({
name: formData.get('name'),
price: formData.get('price'),
stock_quantity: formData.get('stock_quantity'),
})
if (!validation.success) {
return {
errors: validation.error.flatten().fieldErrors,
message: 'Invalid fields. Please check the form.',
}
}
const supabase = await createServerSupabaseClient()
const { data, error } = await supabase
.from('products')
.insert([validation.data])
.select()
.single()
if (error) {
return { message: 'Database error: Failed to create product.' }
}
// Instant cache invalidation
updateTag('products')
redirect(`/products/${data.id}`)
}
Pattern 3: Streaming with Suspense
// app/(dashboard)/dashboard/page.tsx
import { Suspense } from 'react'
export default async function DashboardPage() {
return (
<div>
{/* Static shell - instant */}
<DashboardHeader />
{/* Long-lived cache */}
<Suspense fallback={<StatsSkeleton />}>
<StaticStats />
</Suspense>
{/* Medium cache */}
<Suspense fallback={<ChartsSkeleton />}>
<AnalyticsCharts />
</Suspense>
{/* No cache - real-time */}
<Suspense fallback={<LiveSkeleton />}>
<LiveMetrics />
</Suspense>
</div>
)
}
async function StaticStats() {
'use cache'
cacheLife('days')
cacheTag('static-stats')
const stats = await getStaticStatistics()
return <StatsGrid data={stats} />
}
Pattern 4: Form with Validation & Error Handling
// components/forms/product-form.tsx
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { createProduct } from '@/app/actions/products'
export function ProductForm({ product, categories }) {
const action = product
? updateProduct.bind(null, product.id)
: createProduct
const [state, formAction] = useActionState(action, {})
return (
<form action={formAction} className="space-y-6">
{state.message && (
<div className={state.success ? 'alert-success' : 'alert-error'}>
{state.message}
</div>
)}
<div>
<label htmlFor="name">Product Name *</label>
<input
id="name"
name="name"
type="text"
defaultValue={product?.name}
required
aria-invalid={!!state.errors?.name}
/>
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<SubmitButton />
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save Product'}
</button>
)
}
Pattern 5: Optimistic Updates
// components/optimistic-todo-list.tsx
'use client'
import { useOptimistic, startTransition } from 'react'
import { toggleTodo, deleteTodo } from '@/app/actions/todos'
export function OptimisticTodoList({ todos }) {
const [optimisticTodos, updateOptimisticTodos] = useOptimistic(
todos,
(state, action) => {
switch (action.type) {
case 'toggle':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
case 'delete':
return state.filter(todo => todo.id !== action.id)
default:
return state
}
}
)
const handleToggle = async (id) => {
startTransition(() => {
updateOptimisticTodos({ type: 'toggle', id })
})
await toggleTodo(id)
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<span>{todo.title}</span>
</li>
))}
</ul>
)
}
Pattern 6: Cache Key Optimization (Production Critical)
Golden Rule: Cache on dimensions with FEW unique values, not MANY.
❌ BAD - Low cache utilization:
// Creates 1000s of cache entries (one per user)
async function getUserDashboard(userId: string) {
'use cache: remote'
cacheTag(`user-${userId}`)
// Every user gets their own cache entry = low hit rate
return await fetchUserData(userId)
}
✅ GOOD - High cache utilization:
// Extract user preference, cache by language (10-50 values)
async function WelcomeMessage() {
const language = (await cookies()).get('language')?.value || 'en'
// All English users share the same cache entry
const content = await getCMSContent(language)
return <div>{content.welcome}</div>
}
async function getCMSContent(language: string) {
'use cache: remote'
cacheTag(`cms-${language}`)
cacheLife({ expire: 3600 })
// Creates ~10-50 cache entries (one per language)
// NOT thousands (one per user)
return cms.getContent(language)
}
Real-world optimization example:
// ✅ Cache by category, filter price in memory
async function ProductsPage({ searchParams }) {
const { category, minPrice } = await searchParams
return (
<Suspense fallback={<Loading />}>
<ProductList category={category} minPrice={minPrice} />
</Suspense>
)
}
async function ProductList({ category, minPrice }) {
// Cache only on category (few unique values: electronics, clothing, etc.)
const products = await getCachedProducts(category)
// Filter price in memory instead of creating cache entries
// for every possible price value
const filtered = minPrice
? products.filter(p => p.price >= parseFloat(minPrice))
: products
return <div>{/* render filtered products */}</div>
}
async function getCachedProducts(category: string) {
'use cache: remote'
cacheTag('products', `category-${category}`)
cacheLife({ expire: 3600 })
// High cache utilization: ~10-20 categories vs thousands of price filters
return db.products.findByCategory(category)
}
Cache utilization comparison:
- ❌ Cache by (category + price): 20 categories × 100 price ranges = 2,000 entries
- ✅ Cache by category only: 20 entries (100x better utilization)
Pattern 7: Production Cache Profile System (MyJKKN Pattern)
Standardize cache durations across your team with reusable profiles:
// lib/cache/cache-profiles.ts
export const cacheProfiles = {
hot: {
stale: 60, // 1 minute
revalidate: 300, // 5 minutes
expire: 600 // 10 minutes
},
warm: {
stale: 300, // 5 minutes
revalidate: 900, // 15 minutes
expire: 1800 // 30 minutes
},
cold: {
stale: 3600, // 1 hour
revalidate: 7200, // 2 hours
expire: 14400 // 4 hours
},
static: {
stale: 86400, // 1 day
revalidate: 172800, // 2 days
expire: 604800 // 7 days
}
} as const
export function getCacheProfile(profile: keyof typeof cacheProfiles) {
return cacheProfiles[profile]
}
Usage in data layer:
// app/(routes)/billing/invoices/_data/get-invoices.ts
'use cache'
import { getCacheProfile } from '@/lib/cache'
import { cacheLife, cacheTag } from 'next/cache'
export async function getInvoices(filters) {
// Use warm profile (5-minute freshness)
cacheLife(getCacheProfile('warm'))
cacheTag('invoices')
const supabase = await createClient()
const { data } = await supabase.from('billing_invoices').select('*')
return data
}
Profile selection guide:
- hot (1 min): Payment status, active sessions, real-time notifications
- warm (5 min): Invoices, bills, receipts, student profiles, staff records
- cold (1 hour): Institutions, departments, degrees, master data
- static (1 day): Academic years, semesters, regulations, system config
Pattern 8: Hierarchical Cache Tag System
Organize cache tags by module for easy invalidation:
// lib/cache/cache-tags.ts
export const cacheTags = {
billing: {
invoices: {
list: () => 'invoices',
byId: (id: string) => `invoices-${id}`,
byStudent: (studentId: string) => `invoices-student-${studentId}`,
byInstitution: (institutionId: string) => `invoices-institution-${institutionId}`
},
receipts: {
list: () => 'receipts',
byId: (id: string) => `receipts-${id}`,
byStudent: (studentId: string) => `receipts-student-${studentId}`
}
},
learners: {
profiles: {
list: () => 'learner-profiles',
byId: (id: string) => `learner-profiles-${id}`,
bySection: (sectionId: string) => `learner-profiles-section-${sectionId}`,
byStatus: (status: string) => `learner-profiles-status-${status}`
}
}
}
// Cache invalidation helpers
export const cacheInvalidation = {
// Invalidate all student-related caches at once
student: (studentId: string) => [
cacheTags.learners.profiles.byId(studentId),
cacheTags.billing.invoices.byStudent(studentId),
cacheTags.billing.receipts.byStudent(studentId)
]
}
Usage:
'use cache'
import { cacheTags } from '@/lib/cache'
import { cacheTag, cacheLife } from 'next/cache'
export async function getLearnerProfiles(params) {
cacheLife(getCacheProfile('warm'))
// Apply granular cache tags
cacheTag(cacheTags.learners.profiles.list())
if (params.section_id) {
cacheTag(cacheTags.learners.profiles.bySection(params.section_id))
}
if (params.lifecycle_status) {
cacheTag(cacheTags.learners.profiles.byStatus(params.lifecycle_status))
}
// ... fetch data
}
Server Action invalidation:
'use server'
import { updateTag } from 'next/cache'
import { cacheInvalidation } from '@/lib/cache'
export async function updateStudent(studentId: string, data) {
await db.students.update(data)
// Invalidate all related caches instantly
cacheInvalidation.student(studentId).forEach(tag => updateTag(tag))
}
Pattern 9: "Extract and Pass" for Dynamic APIs
The pattern: Read cookies/headers/searchParams OUTSIDE cached scope, pass values as arguments.
// ❌ WRONG - Build timeout or runtime error
async function Dashboard() {
'use cache'
const cookieStore = await cookies() // ERROR!
const userId = cookieStore.get('userId')?.value
return await fetchDashboard(userId)
}
// ✅ CORRECT - Extract outside, pass to cached function
import { cookies } from 'next/headers'
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
)
}
// Wrapper: Reads cookies OUTSIDE cache
async function Dashboard() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
if (!userId) return <div>Please log in</div>
// Pass userId to cached function
return <CachedDashboard userId={userId} />
}
// Cached: Receives data as argument
async function CachedDashboard({ userId }: { userId: string }) {
'use cache'
cacheLife(getCacheProfile('hot'))
cacheTag(cacheTags.users.byId(userId))
// userId is now part of the cache key
const data = await fetchUserData(userId)
return <DashboardView data={data} />
}
Why it works: When you pass values as arguments, they become part of the cache key. Different users automatically get different cache entries.
Pattern 10: Cache Nesting Rules
Understanding which cache directives can nest inside others:
| Outer Cache | Inner Cache | Valid? | Notes |
|---|---|---|---|
use cache: remote |
use cache: remote |
✅ YES | Remote can nest in remote |
use cache: remote |
use cache |
✅ YES | Remote can contain regular cache |
use cache |
use cache: remote |
✅ YES | Regular cache can contain remote |
use cache |
use cache |
✅ YES | Regular can nest in regular |
use cache: remote |
use cache: private |
❌ NO | Error at runtime |
use cache: private |
use cache: remote |
❌ NO | Error at runtime |
use cache: private |
use cache |
❌ NO | Error at runtime |
Valid nesting example:
// ✅ Valid: Remote inside remote
async function outerRemote() {
'use cache: remote'
cacheLife({ expire: 3600 })
const result = await innerRemote()
return result
}
async function innerRemote() {
'use cache: remote'
cacheLife({ expire: 600 })
return getData()
}
Invalid nesting example:
// ❌ Invalid: Private inside remote
async function outerRemote() {
'use cache: remote'
const result = await innerPrivate() // Error!
return result
}
async function innerPrivate() {
'use cache: private'
return getData()
}
Type Safety with Zod
// types/product.ts
import { z } from 'zod'
export const CreateProductSchema = z.object({
name: z.string()
.min(1, 'Name is required')
.max(255, 'Name is too long'),
price: z.coerce
.number({ invalid_type_error: 'Price must be a number' })
.positive('Price must be positive'),
stock_quantity: z.coerce
.number()
.int('Stock must be a whole number')
.min(0, 'Stock cannot be negative'),
})
export const UpdateProductSchema = CreateProductSchema.partial()
export type CreateProductInput = z.infer<typeof CreateProductSchema>
Supabase Setup
Server Client
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createServerSupabaseClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}
Authentication Utilities
// lib/auth.ts
import { createServerSupabaseClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const supabase = await createServerSupabaseClient()
const { data: { user } } = await supabase.auth.getUser()
return user
})
export async function requireAuth() {
const user = await getCurrentUser()
if (!user) redirect('/login')
return user
}
export async function requireRole(allowedRoles: string[]) {
const user = await requireAuth()
const supabase = await createServerSupabaseClient()
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single()
if (!profile || !allowedRoles.includes(profile.role)) {
redirect('/unauthorized')
}
return { user, role: profile.role }
}
Module Development Workflow
When building a new feature module (e.g., Products, Orders):
- Database Schema - Create table with RLS policies (see
references/database-patterns.md) - Types - Define interfaces and Zod schemas (
types/module.ts) - Data Layer - Create cached data fetching functions (
lib/data/module.ts) - Server Actions - Implement CRUD operations (
app/actions/module.ts) - UI Components - Build forms and lists (
components/module/) - Pages - Create routes with Suspense boundaries (
app/(dashboard)/module/)
Best Practices
DO:
- ✅ Enable
cacheComponentsin next.config.ts - ✅ Use Server Actions for all mutations
- ✅ Apply
use cacheto data fetching functions - ✅ Wrap dynamic content in Suspense boundaries
- ✅ Validate all inputs on server with Zod
- ✅ Use optimistic updates for better UX
- ✅ Implement proper RLS policies
- ✅ Use
updateTagfor instant cache updates - ✅ Use TypeScript strict mode
- ✅ Handle errors gracefully
- ✅ Add multiple cache tags for granular invalidation
- ✅ Stream UI progressively with PPR
DON'T:
- ❌ NEVER use cookies(), headers(), or access searchParams inside 'use cache' scopes
- ❌ NEVER access searchParams or params without await/React.use()
- ❌ NEVER fetch data without Suspense boundary OR 'use cache' directive
- ❌ NEVER nest
'use cache: private'inside'use cache: remote'(or vice versa) - ❌ NEVER cache on high-cardinality dimensions (userId, sessionId, timestamps)
- ❌ NEVER pass Promises to cached functions (causes build timeouts)
- ❌ Over-cache frequently changing data
- ❌ Forget Suspense boundaries for streaming
- ❌ Use Route Handlers for simple mutations
- ❌ Trust client-side validation alone
- ❌ Expose sensitive data in error messages
- ❌ Mix cached and uncached data without Suspense
- ❌ Use edge runtime with Cache Components
- ❌ Hardcode cache durations everywhere (use cache profiles)
See Error Prevention Guide for detailed explanations and correct patterns.
See Production Patterns for real-world MyJKKN examples.
Performance Targets
- First Contentful Paint (FCP): < 1.2s
- Largest Contentful Paint (LCP): < 2.5s
- First Input Delay (FID): < 100ms
- Cumulative Layout Shift (CLS): < 0.1
- Time to First Byte (TTFB): < 600ms
Migration from Next.js 15
Replace Route Segment Config
// BEFORE (Next.js 15)
export const dynamic = 'force-static'
export const revalidate = 3600
// AFTER (Next.js 16)
async function getCachedData() {
'use cache'
cacheLife('hours')
cacheTag('data')
return await fetchData()
}
Update Async Params
// BEFORE (Next.js 15)
export default function Page({ params, searchParams }) {
const id = params.id
}
// AFTER (Next.js 16)
export default async function Page({ params, searchParams }) {
const { id } = await params
}
See references/migration-guide.md for complete migration patterns.
Resources
This skill includes comprehensive reference documentation and automation tools:
references/
Detailed patterns and examples loaded as needed:
error-prevention-guide.md- 🚨 READ THIS FIRST - The three critical errors and how to prevent themproduction-patterns.md- 🏆 NEW - Real-world patterns from MyJKKN production app (cache profiles, hierarchical tags, "extract and pass" pattern)cache-components-patterns.md- Advanced caching strategies, PPR, cache key optimization, invalidation patternsserver-actions-forms.md- Form handling, validation, file uploads, multi-step formsmodule-builder-patterns.md- Complete CRUD module development workflowsmigration-guide.md- Step-by-step Next.js 15 to 16 migrationdatabase-patterns.md- Supabase schemas, RLS policies, indexes, functions
scripts/
Automation tools for common tasks:
init_project.sh- Initialize Next.js 16 project with standard structuregenerate_module.py- Generate CRUD module boilerplatevalidate_structure.py- Validate project follows team standards
assets/
Templates and boilerplate code:
next.config.ts- Optimized Next.js 16 configurationmodule-template/- Complete CRUD module boilerplatesupabase-schema/- Database schema templates
Follow these patterns to build production-ready Next.js 16 applications with optimal performance, security, and developer experience. Reference the detailed documentation in references/ for specific use cases and advanced patterns.