patterntypescriptnextjsMajor
Next.js 15 + Supabase full-stack app — build gotchas and patterns
Viewed 0 times
Next.js 15Supabase SSRuseSearchParams Suspensehtml2pdf typescripttailwind v4 themeroute groupsmiddleware auth
Problem
Building a full-stack Next.js 15 App Router + Supabase app involves several non-obvious gotchas: (1) useSearchParams() in client components causes build failure without Suspense boundary, (2) Supabase client initialization fails during static generation if env vars aren't valid URLs even as placeholders, (3) html2pdf.js requires specific TypeScript type assertions for margin tuples and string literal types, (4) Tailwind CSS v4 uses @import 'tailwindcss' and @theme inline syntax instead of tailwind.config.ts
Solution
For useSearchParams(): wrap the component using it in Suspense and add
export const dynamic = 'force-dynamic' to the page. For Supabase placeholder env vars: use valid-format URLs like https://placeholder.supabase.co and a JWT-format string for the anon key — never use plain text placeholders. For html2pdf.js TypeScript: use as [number, number] for margin tuples, as const for string literals like type/unit/format. For Tailwind v4: define design tokens inside @theme inline {} block in globals.css. Architecture: use route groups ((marketing), (auth), (dashboard)) for separate layouts, three Supabase clients (browser, server with cookies, admin with service role), middleware for auth session refresh and route protection.Why
Next.js 15 App Router with Supabase SSR has several points where build-time behavior differs from dev-time: static generation attempts to render pages that need dynamic data, env var validation happens at import time not runtime, and TypeScript strictness requires explicit type narrowing for libraries with loose types.
Gotchas
- useSearchParams() without Suspense boundary silently works in dev but fails build
- Supabase createBrowserClient validates URL format at import time during static generation
- html2pdf.js margin type is [number, number] not number[] — TypeScript won't infer the tuple
- Tailwind v4 has no tailwind.config.ts — use @theme inline in CSS
Code Snippets
Wrap useSearchParams in Suspense with force-dynamic
// Fix: Suspense boundary for useSearchParams
export const dynamic = 'force-dynamic';
export default function LoginPage() {
return <Suspense fallback={<Loading />}><LoginForm /></Suspense>;
}
function LoginForm() {
const searchParams = useSearchParams();
// ...
}Tailwind v4 design tokens in globals.css
@import "tailwindcss";
@theme inline {
--color-navy-900: #0c1222;
--color-amber: #f59e0b;
--font-sans: 'DM Sans', sans-serif;
}Revisions (0)
No revisions yet.