3.8 KiB
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 personorganization: active tenant or workspacemembership: join record between user and organizationrole: coarse access level such asowner,admin,memberpermissions: 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.tssrc/auth.ts
Keep usage consistent across:
- route handlers
- protected server pages
- shared server utilities
Protection patterns
Protected page
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 <div>Protected page</div>;
}
Protected route handler
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:
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:
- compute filtered nav on the server and pass it into layout state
- expose a lightweight session or membership snapshot to the client
- 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.tssrc/hooks/use-nav.tssrc/components/layout/providers.tsxsrc/components/layout/user-nav.tsxsrc/components/layout/app-sidebar.tsxsrc/app/dashboard/workspaces/**src/app/dashboard/billing/page.tsx
Migrate in this order when possible:
- auth config and protected-route shell
- session-aware layout or providers
- shared membership and RBAC utilities
- page-level and route-level conversions
- navigation and UI cleanup