Auth with Clerk
@shadow-js/clerk loads @clerk/clerk-js on demand and mirrors Clerk's user, session and organization into signals.
@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 |
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.
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.