New clerk-auth
Default user authentication solution. Use this skill for any authentication, login, signup, or user account request unless the user explicitly asks for Replit Auth, Sign in with Replit, or Replit SSO.
git clone https://github.com/alyamenfactory/new
T=$(mktemp -d) && git clone --depth=1 https://github.com/alyamenfactory/new "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.local/skills/clerk-auth" ~/.claude/skills/alyamenfactory-new-clerk-auth && rm -rf "$T"
.local/skills/clerk-auth/SKILL.mdClerk Auth
Set up Clerk authentication with proxy support. Keys are automatically provisioned.
When to Use
- User wants authentication, login, signup, or user accounts (this is the default)
- User wants custom branding on login/signup screens
- User wants their own user database
- Any generic auth request that does not explicitly mention "Replit Auth", "Sign in with Replit", or "Replit SSO"
Setup
Step 1: Provision Clerk App
In the JavaScript sandbox, call the
setupClerkWhitelabelAuth function to provision your Clerk app and set secrets:
const result = await setupClerkWhitelabelAuth(); console.log(result);
Step 2: Copy Proxy Middleware Template
mkdir -p artifacts/api-server/src/middlewares cp .local/skills/clerk-auth/templates/api-server/src/middlewares/clerkProxyMiddleware.ts artifacts/api-server/src/middlewares/clerkProxyMiddleware.ts
Step 3: Install Server Dependencies
pnpm --filter @workspace/api-server add http-proxy-middleware @clerk/express
Step 4: Wire Up app.ts
app.tsIn
artifacts/api-server/src/app.ts, mount the proxy middleware before body parsers (the proxy streams raw bytes). Ensure pino structured logging is set up first — see the Logging section in the pnpm-workspace skill and references/server.md for setup instructions.
import express from "express"; import cors from "cors"; import { clerkMiddleware } from "@clerk/express"; import { CLERK_PROXY_PATH, clerkProxyMiddleware } from "./middlewares/clerkProxyMiddleware"; import router from "./routes"; const app = express(); // pinoHttp structured logging middleware should already be mounted here // (see pnpm-workspace skill / references/server.md for setup) app.use(CLERK_PROXY_PATH, clerkProxyMiddleware()); app.use(cors({ credentials: true, origin: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(clerkMiddleware()); app.use("/api", router); export default app;
Step 5: Install Client Dependencies
pnpm --filter @workspace/<artifact-name> add @clerk/react
Expo Setup (optional)
When the mobile app uses Expo, follow these additional steps to integrate Clerk authentication.
Step 1: Install Expo Dependencies
Expo ecosystem packages (
expo-*) must be version-pinned to match the project's Expo SDK. Running pnpm add without a version resolves to the latest on npm, which may belong to a newer SDK and break the app.
For SDK 54 projects:
pnpm --filter @workspace/<expo-app-artifact-name> add -D @clerk/expo expo-auth-session@~7.0.10 expo-secure-store@~15.0.8 expo-web-browser@~15.0.10 expo-crypto@~15.0.8
If the project already has any of these packages installed at compatible versions, they can be omitted from the command. If
@clerk/expo peer dependency warnings mention other missing expo-* packages not listed above, use this SDK 54 version reference:
| Package | SDK 54 version |
|---|---|
| |
| |
| |
| |
| |
Step 2: Pass the Publishable Key to the Dev Script
In the Expo app's
package.json, prepend the Clerk publishable key as an environment variable to the existing dev script (keep all existing env vars and flags intact):
{ "scripts": { "dev": "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=$CLERK_PUBLISHABLE_KEY <existing dev command>" } }
Step 3: Configure Environment Variables in build.js
build.jsIn
build.js, construct the Clerk proxy URL from the deployment domain and forward all Clerk env vars to the Expo build:
const clerkProxyUrl = process.env.CLERK_PROXY_URL ? `https://${expoPublicDomain}${process.env.CLERK_PROXY_URL}` : ""; const env = { ...process.env, // ...other env vars... EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.CLERK_PUBLISHABLE_KEY || "", EXPO_PUBLIC_CLERK_PROXY_URL: clerkProxyUrl, };
Managing Users and Auth Configuration
Always direct users to the Auth pane in the workspace for viewing/managing users and configuring authentication. NEVER direct users to clerk.com or any external Clerk dashboard.
The Auth pane is accessible from the workspace toolbar. It provides:
- Users tab: View all authenticated users with search, sort, and filtering. See user details (user ID, email, name, last login, account created). Ban or unban users.
- Configure tab: Edit app name and icon shown on the login screen, enable/disable login providers (Google, GitHub, Apple, X, Email), and configure custom OAuth credentials for production so that OAuth consent screens show the user's own branding instead of defaults.
Point the user to the Auth pane when they ask about any of the following:
- Viewing registered/signed-up users
- Banning or unbanning a user
- Enabling or disabling login providers
- Changing the app name or icon on the login screen
- Customizing branding on OAuth/login screens
- Configuring custom OAuth credentials for production
- Any admin or dashboard functionality
IMPORTANT
- The server-side proxy code is production only and is not used in development. Do not ask for or set any env vars related to the proxy. All production setup is handled by a separate system.
- When delegating to the design subagent, be sure to include all of the code instructions/guidance below in the task description so it follows them closely
Home Route Behavior
The artifact's base path (
import.meta.env.BASE_URL) must always be a publicly accessible landing page for unauthenticated users — never redirect them to sign-in or sign-up. Dropping users onto a sign-in screen with no context about the app causes confusion and hurts conversion. Avoid <RedirectToSignIn>, wrapping the homepage in auth-only gates, or any explicit redirect from the base path to /sign-in or /sign-up.
For authenticated users, always redirect the base path to a user-portal view so they land directly in the app without an extra click. After the user signed-out, redirects to the home route rather than the sign-in route.
Example code:
import { Show } from "@clerk/react"; import { Switch, Route, Redirect } from "wouter"; // Inside <WouterRouter base={basePath}>, all route paths and navigation // targets are base-relative. "/" matches {basePath}/, "/user-portal" // navigates to {basePath}/user-portal, etc. function HomeRedirect() { return ( <> <Show when="signed-in"> <Redirect to="/user-portal" /> </Show> <Show when="signed-out"> <Home /> </Show> </> ); } function UserPortal() { return ( <> <Show when="signed-in"> <UserPortalPage /> </Show> <Show when="signed-out"> <Redirect to="/" /> </Show> </> ); } function Router() { return ( <Switch> <Route path="/" component={HomeRedirect} /> <Route path="/user-portal" component={UserPortal} /> {/* Other routes */} </Switch> ); }
After Setup - Web App
1. Set up client routing with Wouter base path
This is vital. Setting
<WouterRouter base={basePath}> makes every location change via wouter's components and hooks (e.g. <Route>, <Redirect to>, setLocation) relative to the base URL. However, Clerk's <SignIn path> and <SignUp path> props read window.location.pathname directly and must be full paths — use `${basePath}/sign-in` and `${basePath}/sign-up`.
Important: The proxy setup does not work with Clerk's hosted pages. You must create dedicated
/sign-in and /sign-up routes in your app to handle OAuth callbacks.
import { useEffect, useRef } from "react"; import { ClerkProvider, SignIn, SignUp, Show, useClerk } from '@clerk/react'; import { Switch, Route, useLocation, Router as WouterRouter } from 'wouter'; import { queryClient } from "./lib/queryClient"; import { QueryClientProvider, useQueryClient } from "@tanstack/react-query"; const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; // NOTE: in dev this env var will be empty, in prod it will be automatically set const clerkProxyUrl = import.meta.env.VITE_CLERK_PROXY_URL; const basePath = import.meta.env.BASE_URL.replace(/\/$/, ""); // Clerk passes full paths to routerPush/routerReplace, but wouter's // setLocation prepends the base — strip it to avoid doubling. function stripBase(path: string): string { return basePath && path.startsWith(basePath) ? path.slice(basePath.length) || "/" : path; } if (!clerkPubKey) { throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY in .env file'); } function SignInPage() { return ( <div style={{ display: 'flex', justifyContent: 'center', marginTop: '2rem' }}> {/* path must be the full browser path — Clerk reads window.location.pathname directly */} <SignIn routing="path" path={`${basePath}/sign-in`} signUpUrl={`${basePath}/sign-up`} /> </div> ); } function SignUpPage() { return ( <div style={{ display: 'flex', justifyContent: 'center', marginTop: '2rem' }}> <SignUp routing="path" path={`${basePath}/sign-up`} signInUrl={`${basePath}/sign-in`} /> </div> ); } // Helps user's webview stay up-to-date when the signed-in user changes by invalidating the QueryClient cache. function ClerkQueryClientCacheInvalidator() { const { addListener } = useClerk(); const queryClient = useQueryClient(); const prevUserIdRef = useRef<string | null | undefined>(undefined); useEffect(() => { const unsubscribe = addListener(({ user }) => { const userId = user?.id ?? null; if ( prevUserIdRef.current !== undefined && prevUserIdRef.current !== userId ) { queryClient.clear(); } prevUserIdRef.current = userId; }); return unsubscribe; }, [addListener, queryClient]); return null; } function ClerkProviderWithRoutes() { const [, setLocation] = useLocation(); return ( <ClerkProvider publishableKey={clerkPubKey} proxyUrl={clerkProxyUrl} routerPush={(to) => setLocation(stripBase(to))} routerReplace={(to) => setLocation(stripBase(to), { replace: true })} > <QueryClientProvider client={queryClient}> <ClerkQueryClientCacheInvalidator /> <Switch> {/* HomeRedirect renders homepage or user portal based on signed-in status. */} <Route path="/" component={HomeRedirect} /> <Route path="/sign-in/*?" component={SignInPage} /> <Route path="/sign-up/*?" component={SignUpPage} /> {/* Add other routes here */} </Switch> </QueryClientProvider> </ClerkProvider> ); } function App() { return ( <WouterRouter base={basePath}> <ClerkProviderWithRoutes /> </WouterRouter> ); } export default App;
2. Protect API routes
Use
getAuth from @clerk/express to check for an authenticated user:
import { getAuth } from "@clerk/express"; const requireAuth = (req: any, res: any, next: any) => { const auth = getAuth(req); const userId = auth?.sessionClaims?.userId || auth?.userId; if (!userId) { return res.status(401).json({ error: "Unauthorized" }); } req.userId = userId; next(); }; app.get("/api/protected", requireAuth, handler);
3. Use auth state in components
Use the
<Show> component to conditionally render based on authentication state:
import { Show } from "@clerk/react"; function MyComponent() { return ( <> {/* Show content only to signed-in users */} <Show when="signed-in"> {/* Protected content */} </Show> {/* Show content only to signed-out users */} <Show when="signed-out"> {/* Login prompt or redirect */} </Show> </> ); }
4. Render user profile
Use
useUser hook to get current authenticated user.
Important: Do not use <UserButton /> by default — the built-in component is not customizable and may expose confusing Clerk-level user management options to end users.
import { useUser } from "@clerk/react"; // Render component with user profile const { user, isLoaded } = useUser();
After Setup - Mobile App
1. Set Up ClerkProvider in the Root Layout
In the top-level
_layout.tsx, wrap the app with ClerkProvider and ClerkLoaded:
import { ClerkProvider, ClerkLoaded } from "@clerk/expo"; import { tokenCache } from "@clerk/expo/token-cache"; import { setBaseUrl } from "@workspace/api-client-react"; const domain = process.env.EXPO_PUBLIC_DOMAIN; if (domain) setBaseUrl(`https://${domain}`); const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!; const proxyUrl = process.env.EXPO_PUBLIC_CLERK_PROXY_URL || undefined; export default function RootLayout() { return ( <ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache} proxyUrl={proxyUrl} > <ClerkLoaded> {/* ...rest of app... */} </ClerkLoaded> </ClerkProvider> ); }
2. Wire Up the Auth Token for API Calls
In a layout that requires authentication (e.g.
(home)/_layout.tsx), use setAuthTokenGetter from @workspace/api-client-react so the generated API client attaches the bearer token to every request:
import { useEffect } from "react"; import { Redirect, Stack } from "expo-router"; import { useAuth } from "@clerk/expo"; import { setAuthTokenGetter } from "@workspace/api-client-react"; export default function HomeLayout() { const { isSignedIn, getToken } = useAuth(); useEffect(() => { setAuthTokenGetter(() => getToken()); }, [getToken]); if (!isSignedIn) return <Redirect href="/(auth)/sign-in" />; return <Stack screenOptions={{ headerShown: false }} />; }
3. Build Custom Authentication Screens
By default, implement Email + Password and Google as sign-in / sign-up options unless the user asks otherwise.
You must build custom authentication screens — native Clerk components are incompatible with Expo Go, so a custom layout is the only viable approach.
All authentication code must follow the Clerk Core v3 SDK APIs documented in the references below. Do not rely on prior knowledge of the Clerk SDK — the Core v3 API has breaking changes from v2, and using outdated patterns will produce runtime errors. Read the relevant reference in full before writing any authentication code.
- For email/password sign-in and sign-up flows, follow
..local/skills/clerk-auth/references/custom-ui/expo-sdk-email-password.md - For Google OAuth (and other OAuth provider) sign-in and sign-up flows, follow
..local/skills/clerk-auth/references/custom-ui/expo-sdk-oauth.md
4. (IMPORTANT) Fix Incompatible Package Versions
ALWAYS fix incompatible package version warnings from the system log. These warnings indicate mismatched peer dependencies that will cause runtime crashes or subtle bugs. Resolve every version conflict before proceeding — do not ignore them.
Environment Variables (Auto-Provisioned)
These are set automatically by
setupClerkWhitelabelAuth(). Do not ask the user for these values.
(server): Auto-provisioned secret keyCLERK_SECRET_KEY
(server): Auto-provisioned publishable keyCLERK_PUBLISHABLE_KEY
(client): Auto-provisioned publishable keyVITE_CLERK_PUBLISHABLE_KEY
Migrating from Replit Auth
When migrating an existing app from Replit Auth to Clerk, read the following references:
— General migration guidance: detection, common rules, user identity mapping, and the criticalreferences/migration.md
requirement for migrated users.sessionClaims.userId
— Web app migration (Express API server + React+Vite frontend): what to remove and what to transition.references/web-migration.md
— Expo mobile app migration: what to remove and what to transition.references/expo-migration.md
Troubleshooting
Unauthorized (401) errors in the preview web app
When API requests from the web app return 401 Unauthorized, calling
is never the correct fix for web applications. That function exists only for mobile (Expo) apps where cookie-based sessions are unavailable. In the web app, the browser automatically sends session cookies with every API request, so authentication should work without any explicit token handling.setAuthTokenGetter
Instead:
- Check middleware setup — Verify that
is mounted inclerkMiddleware()
before the API routes, and that theapp.ts
middleware is correctly wired on protected endpoints (checkingrequireAuth
for a valid session).getAuth(req) - When not sure, add debugging logs — Add temporary logging inside the
middleware (e.g. log the output ofrequireAuth
) to see what Clerk is receiving. Then restart the client and API server workflows and ask the user to test again so you can inspect the logs.getAuth(req)
Limitations
- Idempotent: Safe to call multiple times
- Proxy path is hardcoded to
/__clerk