// Build production-ready Next.js 16 + Supabase authentication and role-based access control systems with 4-tier user roles (admin, main_user, vip_user, user). Use this skill when implementing authentication, user management, role-based permissions, or RLS policies in Next.js projects.
| name | nextjs-supabase-auth |
| description | Build production-ready Next.js 16 + Supabase authentication and role-based access control systems with 4-tier user roles (admin, main_user, vip_user, user). Use this skill when implementing authentication, user management, role-based permissions, or RLS policies in Next.js projects. |
Build production-ready authentication and authorization systems for Next.js 16 applications using Supabase. This skill provides battle-tested patterns for user authentication, 4-tier role systems, and Row Level Security (RLS) policies.
Invoke this skill when:
Supabase in Next.js requires three separate client configurations:
lib/supabase/client.ts) - For client componentslib/supabase/server.ts) - For server components and server actionslib/supabase/middleware.ts) - For session refresh in middlewareCritical: Never use the browser client in server components or vice versa. This causes hydration errors and authentication failures.
Four-tier role system with numerical hierarchy:
admin (4) - Full system access, user management
main_user (3) - Extended privileges (project-specific)
vip_user (2) - Premium features (project-specific)
user (1) - Basic access
Permission checks use hasMinimumRole() to verify hierarchical access.
.env.local with credentials:NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
npm install @supabase/supabase-js @supabase/ssr
Execute SQL schemas in Supabase SQL Editor in this exact order:
Base Schema (references/database-schema.md section "Base Schema")
profiles, posts, comments tablesRole System (references/database-schema.md section "Role System Extension")
role column to profilesPromote First Admin (scripts/setup-admin.sql)
Reference Implementation: See assets/templates/ directory for complete code.
Browser Client (src/lib/supabase/client.ts):
createBrowserClient from @supabase/ssrServer Client (src/lib/supabase/server.ts):
createServerClient from @supabase/ssrcookies() for Next.js 16 compatibilityMiddleware Client (src/lib/supabase/middleware.ts):
updateSession() functionsupabase.auth.getUser() to refresh sessionCreate middleware.ts in project root:
import { updateSession } from '@/lib/supabase/middleware';
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
};
Critical: This refreshes user sessions on every request. Without it, users will be logged out unexpectedly.
Two-File Pattern: Separate server and client permissions to avoid hydration errors.
Server Permissions (src/lib/permissions.ts):
getCurrentUserProfile() - Fetch authenticated user's profileisSystemAdmin(), isAdmin(), isVIP() - Role checkshasMinimumRole() - Hierarchical permission verificationClient Permissions (src/lib/permissions-client.ts):
hasRole(), getRoleDisplayName(), getRoleBadgeColor()Reference: See assets/templates/permissions.ts and assets/templates/permissions-client.ts
Login Flow:
src/app/login/page.tsx with LoginForm componentsupabase.auth.signInWithPassword()router.refresh() to update server componentsSignup Flow:
src/app/signup/page.tsx with SignupForm componentsupabase.auth.signUp() with emailRedirectTo optionCallback Handler:
src/app/auth/callback/route.tsexchangeCodeForSession()Reference: See assets/templates/auth/ directory
Apply RLS policies at the database level for security:
Read Operations: Use USING clause
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
Write Operations: Use WITH CHECK clause
CREATE POLICY "Users can create posts as themselves"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
Admin Override: Role-based policies
CREATE POLICY "Admins can manage all posts"
ON posts FOR ALL
USING ((SELECT role FROM profiles WHERE id = auth.uid()) = 'admin');
Pattern for Admin-Only Pages:
import { getCurrentUserProfile, isSystemAdmin } from '@/lib/permissions';
import { redirect } from 'next/navigation';
export default async function AdminPage() {
const profile = await getCurrentUserProfile();
if (!profile || !isSystemAdmin(profile)) {
redirect('/');
}
// Admin-only content
}
Pattern for Role-Based Access:
const profile = await getCurrentUserProfile();
if (!profile || !hasMinimumRole(profile, 'vip_user')) {
return <AccessDenied />;
}
Problem: In Next.js 16, params in dynamic routes are Promises.
Solution:
// ❌ Next.js 15 (breaks in 16)
export default function Page({ params }: { params: { id: string } }) {
const { id } = params;
}
// ✅ Next.js 16
export default async function Page({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
}
Problem: cookies() is now async in Next.js 16.
Solution: Always await cookies() in server client creation:
// ✅ Correct
export async function createClient() {
const cookieStore = await cookies();
// ...
}
Problem: Using server-only code in client components causes errors.
Solution:
getCurrentUserProfile() in server-only permissions.tspermissions-client.ts for client componentsimport { getCurrentUserProfile } from '@/lib/permissions';
export default async function ServerComponent() {
const profile = await getCurrentUserProfile();
if (!profile) {
// User not logged in
}
// Use profile.role, profile.email, etc.
}
'use client';
import { getRoleDisplayName, getRoleBadgeColor } from '@/lib/permissions-client';
export default function UserBadge({ profile }: { profile: Profile }) {
return (
<span className={getRoleBadgeColor(profile.role)}>
{getRoleDisplayName(profile.role)}
</span>
);
}
'use server';
import { createClient } from '@/lib/supabase/server';
import { getCurrentUserProfile, isSystemAdmin } from '@/lib/permissions';
import { revalidatePath } from 'next/cache';
export async function updateUserRole(userId: string, newRole: UserRole) {
const profile = await getCurrentUserProfile();
if (!profile || !isSystemAdmin(profile)) {
throw new Error('Unauthorized');
}
const supabase = await createClient();
const { error } = await supabase
.from('profiles')
.update({ role: newRole })
.eq('id', userId);
if (error) throw error;
revalidatePath('/admin/users');
}
setup-admin.sql - SQL to promote first user to admin roleverify-setup.sql - Diagnostic queries to verify configurationdatabase-schema.md - Complete SQL schema with explanationsfile-structure.md - Detailed project structure guidetroubleshooting.md - Common issues and solutionstemplates/ - Copy-paste ready code templates
supabase/client.ts - Browser client implementationsupabase/server.ts - Server client implementationsupabase/middleware.ts - Middleware implementationpermissions.ts - Server permissionspermissions-client.ts - Client permissionstypes.ts - TypeScript definitionsauth/ - Authentication componentsUserRole type in src/lib/types.tsroleHierarchy in both permission filessupabase.auth.signInWithOAuth():await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${location.origin}/auth/callback`,
},
});
profiles tableProfile type in src/lib/types.tsCause: Missing or incorrect middleware configuration.
Fix: Verify middleware.ts exists and calls updateSession(). Check matcher pattern excludes static files.
Cause: Using server client in client component, or not awaiting cookies().
Fix: Ensure createClient() from correct import. Verify await cookies() in server client.
Cause: Policy order, missing policy, or incorrect role check.
Fix:
ALTER TABLE table_name ENABLE ROW LEVEL SECURITYCause: Trigger not configured or failed.
Fix:
Get current user in server component:
const profile = await getCurrentUserProfile();
Check admin access:
if (!isSystemAdmin(profile)) redirect('/');
Check minimum role:
if (!hasMinimumRole(profile, 'vip_user')) return <AccessDenied />;
Foreign key to user:
author_id UUID REFERENCES profiles(id)
Server action pattern:
'use server';
const supabase = await createClient();
Client component pattern:
'use client';
const supabase = createClient();