[]previous and next page

Auth with Clerk

@shadow-js/clerk loads @clerk/clerk-js on demand and mirrors Clerk's user, session and organization into signals.

For agentsUpdated

@shadow-js/clerk loads @clerk/clerk-js on demand and mirrors Clerk's user, session and organization into signals. Wrap the app in ClerkProvider once (in src/app.tsx); every hook returns accessors, so read them with a call.

@clerk/nextjs @shadow-js/clerk
<ClerkProvider> <ClerkProvider publishableKey={...}>; the key must start with pk_test_ or pk_live_
<SignedIn>, <SignedOut> Same names. Both show their fallback until Clerk has loaded
<Protect role="org:admin"> <Protect role="org:admin" fallback={...}>, also permission and condition
useAuth() useAuth(): isLoaded, isSignedIn, userId, sessionId, orgId, orgRole are accessors; getToken, signOut are functions
useUser() useUser(): user is an accessor (undefined while loading, null signed out)
<SignInButton>, <SignUpButton>, <SignOutButton> Same names. A child element becomes the button
<UserButton>, <SignIn>, <SignUp>, <UserProfile>, <OrganizationSwitcher> Same names; they mount Clerk's UI into a div on the client
auth(), currentUser() on the server getAuth(event) after clerkMiddleware, or authenticateRequest(request, keys) / requireAuth(request, keys) from @shadow-js/clerk/server; there is no currentUser()
clerkMiddleware() and route matchers clerkMiddleware(keys) from @shadow-js/clerk/server in the Dusk middleware array. It authenticates every request and stores the outcome; it does not block anything by itself. Protect routes in beforeLoad or a server function
src/auth.tsxtsx
import {
  ClerkProvider,
  Protect,
  RedirectToSignIn,
  SignedIn,
  SignedOut,
  SignInButton,
  SignOutButton,
  useAuth,
} from "@shadow-js/clerk";
import type { ClerkLike } from "@shadow-js/clerk";

/** In an app this is `import.meta.env.DUSK_PUBLIC_CLERK_PUBLISHABLE_KEY`. */
const publishableKey = "pk_test_Y2xlcmsuZXhhbXBsZS5jb20k";

export function Header() {
  const auth = useAuth();
  return (
    <header>
      <p>{auth.isSignedIn() ? `Signed in as ${auth.userId()}` : "Guest"}</p>
      <SignedIn>
        <SignOutButton />
      </SignedIn>
      <SignedOut>
        <SignInButton />
      </SignedOut>
    </header>
  );
}

export function Members() {
  return (
    <Protect fallback={<p>Members only</p>}>
      <p>Members area</p>
    </Protect>
  );
}

export function Account() {
  return (
    <Protect fallback={<RedirectToSignIn />}>
      <p>Account</p>
    </Protect>
  );
}

/** Pass `clerk` only in tests or when your app creates the Clerk instance itself. */
export function AuthRoot(props: { clerk?: ClerkLike; children?: unknown }) {
  return (
    <ClerkProvider publishableKey={publishableKey} {...(props.clerk === undefined ? {} : { clerk: props.clerk })}>
      {props.children}
    </ClerkProvider>
  );
}

Route protection#

Component-level protection is SignedIn, SignedOut and Protect with RedirectToSignIn as the fallback. To protect a route (the closest thing to a middleware.ts matcher), check auth in beforeLoad, which runs top-down before the loaders and may throw redirect(). In the browser beforeLoad runs on client navigations too, so Clerk must be loaded before the router is created: create the Clerk instance yourself, await clerk.load(), then give it to both ClerkProvider (its clerk prop) and the router context.

src/protected.tsxtsx
import { createMemoryHistory, createRouter, defineRoute, Outlet, redirect, RouterProvider } from "@shadow-js/router";
import type { ClerkLike } from "@shadow-js/clerk";

function signedIn(context: Readonly<Record<string, unknown>>): boolean {
  const clerk = context["clerk"] as ClerkLike | undefined;
  return clerk?.session?.status === "active";
}

const account = defineRoute("/account", {
  beforeLoad: ({ context }) => {
    if (!signedIn(context)) throw redirect("/sign-in");
  },
  component: () => <p>Account</p>,
});

const signIn = defineRoute("/sign-in", { component: () => <p>Please sign in</p> });

export function createAppRouter(clerk: ClerkLike, url: string) {
  return createRouter({
    routes: [account, signIn],
    history: createMemoryHistory({ initialEntries: [url] }),
    context: { clerk },
  });
}

export function ProtectedApp(props: { router: ReturnType<typeof createAppRouter> }) {
  return (
    <RouterProvider router={props.router}>
      <Outlet />
    </RouterProvider>
  );
}

Clerk on the server#

@shadow-js/clerk/server is built on @clerk/backend. clerkMiddleware(keys) fits the Dusk middleware signature: it authenticates each request and stores the outcome, and getAuth(event) reads it anywhere below, including a server function through getRequestEvent(). authenticateRequest returns a result and never throws for a missing or invalid token (it is signed out, with a reason); requireAuth rejects with a Response: a 302 to signInUrl when given, else 401. Keys come from the environment; put authorizedParties in production, or a token minted for another application of the same Clerk instance is accepted. The block uses placeholder keys with the right shape and a request without a token, which @clerk/backend answers offline; the signature checks are Clerk's.