Authentication patterns for Next.js apps using Clerk, including proxy/middleware setup, role-based access, organization scoping, and dev-mode fallbacks.
Clerk provides authentication, user management, and organization-based RBAC for Next.js apps. In Neexo projects, Clerk handles sign-in, sign-up, organization switching, and role enforcement.
Next.js 16: proxy.ts vs middleware.ts
Prefer proxy.ts (named proxy export) for Node.js Clerk auth on Next.js 16:
Edge caveat:proxy.ts runs in the Node.js runtime. If you need the Edge runtime (common with locale middleware such as next-intl), keep or compose middleware.ts on Edge and follow Clerk + i18n docs for that stack. Do not assume every app can delete middleware.ts.
Auth Context Helper
import { auth } from "@clerk/nextjs/server";
export async function getAuthContext() {
const { userId, orgId, orgRole } = await auth();
if (!userId || !orgId) {
throw new Error("Unauthorized");
}
return { userId, orgId, orgRole };
}
Role-Based Access
const STUDIO_ROLES = new Set(["org:admin", "org:editor"]);
export function hasStudioAccess(orgRole: string | undefined) {
return orgRole ? STUDIO_ROLES.has(orgRole) : false;
}
// In an API route or server action:
const { orgRole } = await getAuthContext();
if (!hasStudioAccess(orgRole)) {
return new Response("Forbidden", { status: 403 });
}
Organization Scoping
Every database query must filter by organization:
// Correct — always scope by orgId
const items = await db.select().from(itemsTable).where(eq(itemsTable.orgId, orgId));
// Wrong — returns data across all organizations
const items = await db.select().from(itemsTable);
Do not call auth() in client components — it's server-only
Use useUser() and useOrganization() for client-side auth state
Wrap Clerk components (UserButton, OrganizationSwitcher) in safe client wrappers
Never expose CLERK_SECRET_KEY to the client — only NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
Do not force proxy.ts when Edge middleware is required for i18n or other Edge-only work
Raw content
Copy into your project — e.g. .instructions.md, .agent.md, or SKILL.md
## Overview
Clerk provides authentication, user management, and organization-based RBAC for Next.js apps. In Neexo projects, Clerk handles sign-in, sign-up, organization switching, and role enforcement.
## Next.js 16: proxy.ts vs middleware.ts
Prefer `proxy.ts` (named `proxy` export) for **Node.js** Clerk auth on Next.js 16:
```tsx
// src/proxy.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)", "/api/public(.*)"]);
export const proxy = clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
export const config = {
matcher: ["/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)"],
};
```
**Edge caveat:** `proxy.ts` runs in the Node.js runtime. If you need the **Edge** runtime (common with locale middleware such as next-intl), keep or compose `middleware.ts` on Edge and follow Clerk + i18n docs for that stack. Do not assume every app can delete `middleware.ts`.
## Auth Context Helper
```tsx
import { auth } from "@clerk/nextjs/server";
export async function getAuthContext() {
const { userId, orgId, orgRole } = await auth();
if (!userId || !orgId) {
throw new Error("Unauthorized");
}
return { userId, orgId, orgRole };
}
```
## Role-Based Access
```tsx
const STUDIO_ROLES = new Set(["org:admin", "org:editor"]);
export function hasStudioAccess(orgRole: string | undefined) {
return orgRole ? STUDIO_ROLES.has(orgRole) : false;
}
// In an API route or server action:
const { orgRole } = await getAuthContext();
if (!hasStudioAccess(orgRole)) {
return new Response("Forbidden", { status: 403 });
}
```
## Organization Scoping
Every database query must filter by organization:
```tsx
// Correct — always scope by orgId
const items = await db.select().from(itemsTable).where(eq(itemsTable.orgId, orgId));
// Wrong — returns data across all organizations
const items = await db.select().from(itemsTable);
```
## Dev Mode Fallback
For local development without Clerk keys:
```tsx
const DEV_USER_ID = "dev-user";
const DEV_ORG_ID = "dev-org";
const DEV_ORG_ROLE = "org:admin";
export async function getAuthContext() {
if (process.env.NODE_ENV === "development" && !process.env.CLERK_SECRET_KEY) {
return { userId: DEV_USER_ID, orgId: DEV_ORG_ID, orgRole: DEV_ORG_ROLE };
}
// ... real auth
}
```
## Common Pitfalls
- Do not call `auth()` in client components — it's server-only
- Use `useUser()` and `useOrganization()` for client-side auth state
- Wrap Clerk components (UserButton, OrganizationSwitcher) in safe client wrappers
- Never expose `CLERK_SECRET_KEY` to the client — only `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`
- Do not force `proxy.ts` when Edge middleware is required for i18n or other Edge-only work