# Auth.js + Organization RBAC Guide Use this guide when the task involves authentication, session-aware pages, or replacing Clerk-coupled access control. ## Target model This repo should evolve toward an app-owned model: - `user`: authenticated person - `organization`: active tenant or workspace - `membership`: join record between user and organization - `role`: coarse access level such as `owner`, `admin`, `member` - `permissions`: optional fine-grained capabilities Auth.js authenticates the user. Your app owns the organization and RBAC data. ## Auth boundary Prefer a single auth boundary that exports: - Auth.js config - `auth()` session helper - provider definitions - callbacks that enrich the session with app-owned ids if needed Typical locations: - `auth.ts` - `src/auth.ts` Keep usage consistent across: - route handlers - protected server pages - shared server utilities ## Protection patterns ### Protected page ```tsx import { auth } from '@/auth'; import { redirect } from 'next/navigation'; export default async function BillingPage() { const session = await auth(); if (!session?.user) { redirect('/auth/sign-in'); } return
Protected page
; } ``` ### Protected route handler ```tsx import { NextResponse } from 'next/server'; import { auth } from '@/auth'; export async function POST() { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); } return NextResponse.json({ ok: true }); } ``` ## Membership checks Prefer a reusable server helper for membership resolution so page and API protection behave the same way. Good responsibilities for a helper: - require an authenticated session - look up the active organization - load the user's membership - optionally enforce role or permissions Example shape: ```tsx export async function requireOrganizationAccess(options?: { role?: 'owner' | 'admin' | 'member'; permission?: string; }) { const session = await auth(); if (!session?.user?.id) throw new Error('Unauthorized'); const membership = await findActiveMembership(session.user.id); if (!membership) throw new Error('Organization membership required'); if (options?.role && membership.role !== options.role) { throw new Error('Forbidden'); } return { session, membership }; } ``` ## Navigation migration The current repo uses Clerk hooks in `src/hooks/use-nav.ts`. Treat this as legacy. Target behavior: - nav visibility comes from app-owned session or membership data - client rendering may consume a serialized access snapshot - server pages and route handlers still enforce real access independently If the task touches navigation, prefer one of these approaches: 1. compute filtered nav on the server and pass it into layout state 2. expose a lightweight session or membership snapshot to the client 3. keep client filtering, but source it from app-owned data rather than Clerk hooks ## Plan and feature gates Do not map old Clerk Billing language directly into Auth.js. Instead: - store plan state on `organizations`, `subscriptions`, or entitlement tables - expose a server helper such as `requirePlan('pro')` - keep the page or API gate server-side If there is no billing provider yet, document it as a placeholder and do not over-promise implementation detail. ## Migration advice for this repo Clerk hot spots currently include: - `src/proxy.ts` - `src/hooks/use-nav.ts` - `src/components/layout/providers.tsx` - `src/components/layout/user-nav.tsx` - `src/components/layout/app-sidebar.tsx` - `src/app/dashboard/workspaces/**` - `src/app/dashboard/billing/page.tsx` Migrate in this order when possible: 1. auth config and protected-route shell 2. session-aware layout or providers 3. shared membership and RBAC utilities 4. page-level and route-level conversions 5. navigation and UI cleanup