This is a Next.js 15 + Supabase + Stripe application using the App Router, TypeScript strict mode, and Tailwind CSS. All server-side auth MUST use getUser(), never getSession().
- Next.js 15 (App Router only — no Pages Router)
- React 19
- TypeScript (strict mode)
- Supabase (auth, database, storage)
- Stripe (payments via Checkout Sessions)
- Tailwind CSS + shadcn/ui
- Zod (validation at every boundary)
npm run dev # Start dev server
npm run build # Production build (catches type errors)
npm run lint # ESLint
npx tsc --noEmit # Type check without emitting<security_critical>
-
NEVER use
supabase.auth.getSession()in server-side code (Server Components, Server Actions, Route Handlers, middleware). It reads the JWT from cookies WITHOUT verifying it — a forged token passes silently. ALWAYS usesupabase.auth.getUser()which makes a verification call to the Supabase auth server. -
NEVER import from
@supabase/auth-helpers-nextjs— it is DEPRECATED. Always use@supabase/ssrfor bothcreateBrowserClient(client) andcreateServerClient(server). -
NEVER put auth enforcement logic in
middleware.ts. Middleware runs on Edge Runtime and cannot securely verify Supabase JWTs. Middleware should ONLY callupdateSession()to refresh tokens. Auth enforcement MUST happen in layouts/pages viagetUser(). -
NEVER build a custom credit card form. ALWAYS redirect to Stripe Checkout. Custom forms create PCI compliance liability.
-
NEVER handle Stripe webhook events without calling
stripe.webhooks.constructEvent()first to verify thestripe-signatureheader. Without this, anyone can POST fake events to your webhook endpoint. -
NEVER upload files directly from client code using the Supabase anon key — it allows writing to any storage path. ALWAYS generate signed upload URLs server-side, scoped to
user.id. -
Row Level Security (RLS) MUST be enabled on EVERY Supabase table. Without RLS, the anon key (public in every client bundle) can read ALL rows from any table.
-
NEVER expose raw error messages, stack traces, or database errors to the client. Log server-side, return
{ error: "Internal server error" }to the client. </security_critical>
<nextjs15_breaking_changes>
paramsandsearchParamsare PROMISES in Next.js 15. You MUST await them:
// CORRECT
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}
// WRONG — compiles but crashes at runtime
export default function Page({ params }: { params: { id: string } }) {
const id = params.id // TypeError at runtime
}-
After mutations (Server Actions, Route Handlers), ALWAYS call
revalidatePath()orrevalidateTag(). Next.js 15 aggressively caches — stale data is the default without explicit revalidation. -
Use Server Components by default. Only add
'use client'when the component genuinely needs browser APIs (useState, useEffect, onClick handlers, window/document access). -
NEVER use
Math.random(),Date.now(), ornew Date()directly in Server Components for rendering. These cause hydration mismatches. Use them inuseEffector pass from the server as props. </nextjs15_breaking_changes>
<api_validation>
-
EVERY Route Handler and Server Action MUST validate input with Zod before any business logic. Raw
request.json()access without validation is a crash and injection risk. -
Auth check ALWAYS happens BEFORE input validation. Order: authenticate -> validate -> execute.
-
Return consistent error shapes:
{ error: string, details?: unknown }. Never expose internal error messages. </api_validation>
<code_patterns>
-
TypeScript: NEVER use
any. Useunknown+ type narrowing or Zod inference. -
Always use
cn()from@/lib/utilsfor conditional Tailwind classes (wraps clsx + tailwind-merge). -
Server Actions must return
ActionResponse<T>—{ success: true, data: T } | { success: false, error: string }. -
File naming: kebab-case for files, PascalCase for React components, camelCase for utilities.
-
Import order: React/Next -> external packages -> @/ aliases -> relative imports -> types. </code_patterns>