System Overview¶
!!! info "TL;DR" Next.js App Router with Server Components. Server Actions for mutations. Route Handlers for API. Middleware for auth and i18n. Three Supabase client variants for different contexts.
Next.js App Router¶
The application uses Next.js 15's App Router, which is built on React Server Components. This architecture divides components into two categories:
Server Components (Default)¶
Most components are Server Components. They:
- Render on the server (no JavaScript sent to client)
- Can directly access databases and APIs
- Cannot use React hooks like
useState,useEffect - Cannot use browser APIs
Example:
// app/[locale]/page.tsx (Server Component)
import { createServerClient } from '@/lib/supabase/server'
import { getTranslations } from 'next-intl/server'
export default async function SubmissionsPage() {
const supabase = await createServerClient()
const t = await getTranslations('submissions')
const { data: submissions } = await supabase
.from('submissions')
.select('*')
.order('vote_count', { ascending: false })
.limit(50)
return (
<div>
<h1>{t('title')}</h1>
{submissions.map(s => (
<SubmissionCard key={s.id} submission={s} />
))}
</div>
)
}
Benefits:
- No client JavaScript for static content
- Direct database access (no API route needed)
- Better SEO (fully rendered HTML)
- Faster initial page load
Client Components¶
Client Components are marked with 'use client' directive. They:
- Run in the browser
- Can use React hooks and browser APIs
- Required for interactivity (forms, animations, realtime)
- Import Server Components as children
Example:
// components/submissions/vote-button.tsx (Client Component)
'use client'
import { useState } from 'react'
import { voteAction } from '@/app/[locale]/submissions/[id]/actions'
export function VoteButton({ submissionId, initialVotes }) {
const [votes, setVotes] = useState(initialVotes)
const [loading, setLoading] = useState(false)
const handleVote = async () => {
setLoading(true)
const result = await voteAction(submissionId)
setVotes(result.vote_count)
setLoading(false)
}
return (
<button onClick={handleVote} disabled={loading}>
▲ {votes}
</button>
)
}
Server Actions¶
Server Actions are server-side functions that can be called from Client Components. They replace traditional API routes for form submissions and mutations.
Defining a Server Action:
// app/[locale]/submit/actions.ts
'use server'
import { createServerClient } from '@/lib/supabase/server'
import { submissionSchema } from '@/types/submission'
import { revalidatePath } from 'next/cache'
export async function createSubmission(formData: FormData) {
// 1. Validate input
const parsed = submissionSchema.safeParse({
title: formData.get('title'),
description: formData.get('description'),
type: formData.get('type'),
})
if (!parsed.success) {
return { error: parsed.error.flatten() }
}
// 2. Authenticate
const supabase = await createServerClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return { error: 'Authentication required' }
}
// 3. Insert
const { data, error } = await supabase
.from('submissions')
.insert({
...parsed.data,
author_id: user.id,
})
.select()
.single()
if (error) {
return { error: error.message }
}
// 4. Revalidate cache
revalidatePath('/[locale]', 'page')
return { success: true, submission: data }
}
Calling from Client Component:
'use client'
import { createSubmission } from './actions'
export function SubmissionForm() {
return (
<form action={createSubmission}>
<input name="title" />
<textarea name="description" />
<button type="submit">Submit</button>
</form>
)
}
Benefits:
- Type-safe (TypeScript end-to-end)
- No API route boilerplate
- Automatic CSRF protection
- Progressive enhancement (works without JavaScript)
Route Handlers¶
For API endpoints (REST API, webhooks, MCP tools), use Route Handlers:
// app/api/submissions/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createServerClient } from '@/lib/supabase/server'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const status = searchParams.get('status')
const supabase = await createServerClient()
let query = supabase.from('submissions').select('*')
if (status) {
query = query.eq('status', status)
}
const { data, error } = await query
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ data })
}
export async function POST(request: NextRequest) {
const body = await request.json()
// Handle POST...
}
Middleware¶
Middleware runs before every request. Used for auth and i18n.
// middleware.ts
import { createServerClient } from '@/lib/supabase/middleware'
import createIntlMiddleware from 'next-intl/middleware'
import { NextRequest, NextResponse } from 'next/server'
const intlMiddleware = createIntlMiddleware({
locales: ['en', 'es', 'fr', 'de', 'ru', 'pt-BR', 'ja', 'it'],
defaultLocale: 'en',
})
export async function middleware(request: NextRequest) {
// 1. Handle i18n
let response = intlMiddleware(request)
// 2. Validate auth
const { supabase, response: authResponse } = await createServerClient(request)
const {
data: { session },
} = await supabase.auth.getSession()
if (session) {
// Set user ID header for downstream use
authResponse.headers.set('x-user-id', session.user.id)
}
// 3. Protected routes
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!session) {
return NextResponse.redirect(new URL('/auth/login', request.url))
}
// Check if user is team member
const { data: teamMember } = await supabase
.from('team_members')
.select('id')
.eq('user_id', session.user.id)
.single()
if (!teamMember) {
return NextResponse.redirect(new URL('/', request.url))
}
}
return authResponse
}
export const config = {
matcher: ['/((?!_next|_static|_vercel|favicon.ico).*)'],
}
Supabase Client Variants¶
Three client variants for different contexts:
1. Server Client (Server Components, Server Actions)¶
// lib/supabase/server.ts
import { createServerClient as createClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createServerClient() {
const cookieStore = await cookies()
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookies) => {
cookies.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options)
})
},
},
}
)
}
2. Browser Client (Client Components)¶
// lib/supabase/client.ts
import { createBrowserClient as createClient } from '@supabase/ssr'
export function createBrowserClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
3. Admin Client (Service Role)¶
// lib/supabase/admin.ts
import { createClient } from '@supabase/supabase-js'
export function createAdminClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Bypasses RLS
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
)
}
Use admin client sparingly. Most operations should go through RLS-protected clients.
Request Lifecycle¶
1. Page Request (Server Component)¶
Browser → Vercel Edge → Middleware (auth, i18n)
↓
Next.js Server → Server Component
↓
Supabase Client (server) → PostgreSQL (RLS applied)
↓
HTML rendered → Streamed to browser
2. Form Submission (Server Action)¶
Browser → POST /action → Server Action
↓
Validate input (Zod) → Authenticate (Supabase Auth)
↓
Mutation → PostgreSQL (RLS applied)
↓
Revalidate cache → Return result → Browser
3. API Request (Route Handler)¶
Client → GET /api/submissions → Route Handler
↓
Authenticate (Bearer token) → Query database
↓
JSON response → Client
File Structure¶
src/
├── app/
│ ├── [locale]/ # Localized routes
│ │ ├── page.tsx # Server Component
│ │ ├── layout.tsx # Layout wrapper
│ │ ├── submit/
│ │ │ ├── page.tsx # Server Component (form UI)
│ │ │ └── actions.ts # Server Actions
│ │ └── admin/
│ │ ├── layout.tsx # Admin layout
│ │ └── page.tsx # Admin dashboard
│ └── api/ # Route Handlers
│ ├── submissions/
│ │ └── route.ts
│ └── mcp/
│ └── tools/route.ts
├── components/
│ ├── submissions/ # Domain components
│ │ ├── card.tsx # Server Component
│ │ └── vote-button.tsx # Client Component ('use client')
│ └── ui/ # shadcn/ui components
├── lib/
│ ├── supabase/ # Client factories
│ │ ├── server.ts
│ │ ├── client.ts
│ │ └── admin.ts
│ └── utils.ts
├── types/
│ ├── database.ts # Supabase generated types
│ └── submission.ts # Zod schemas
└── middleware.ts # Global middleware
What's Next¶
- Database Schema — Tables and RLS policies
- Authentication — Session management, API keys
- Realtime — Live updates via Supabase