Clerk Authentication

Authentication patterns for Next.js apps using Clerk, including middleware setup, role-based access, organization scoping, and dev-mode fallbacks.

AuthorNeexoCore
Apply to**/*.{ts,tsx}, **/proxy.ts, **/middleware.ts
Updated
authclerknextjs

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 Middleware

Next.js 16 uses proxy.ts (named proxy export) instead of middleware.ts:

// 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)).*)"],
};

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(machines).where(eq(machines.orgId, orgId));

// ❌ Wrong — returns data across all organizations
const items = await db.select().from(machines);

Dev Mode Fallback

For local development without Clerk keys:

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

Raw content

Copy this 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 Middleware

Next.js 16 uses `proxy.ts` (named `proxy` export) instead of `middleware.ts`:

```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)).*)"],
};
```

## 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(machines).where(eq(machines.orgId, orgId));

// ❌ Wrong — returns data across all organizations
const items = await db.select().from(machines);
```

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