Files
phaichayon 7a390cf0df init
2026-06-11 12:46:57 +07:00

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 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

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:

  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