Universal Auth with Clerk
npx @fullproduct/universal-app install with/auth-clerkInstalls the with/auth-clerk plugin branch and Pull-Request.
This enables universal authentication by adding the @auth/clerk workspace package to your monorepo:
- ClerkAuthManager.(next|expo).tsx
- <ClerkProvider /> (wrapper)
- ClerkAuthContext.tsx
- compat.ts
- <SignUp />
- <SignIn />
- <UserButton />
- <OrganizationSwitcher />
- useSignIn()
- useSignUp()
- useClerk()
- useAuth()
- useUser()
- useSession()
- useOrganization()
- useSocialAuthFlow()
- useSignUpFlow()
- useSignInFlow()
- getMobileAuthHeaders()
Requirements - Clerk Setup
Create a free Clerk account at clerk.com/signup . The free tier includes up to 10k monthly active users and does not charge for users who sign up but never return.
Wrap each app root with ClerkAuthManager:
Expo
import { ClerkAuthManager } from '@auth/clerk/expo'Clerk Env Vars
Connect your apps by adding env vars to each app’s .env file:
Expo — apps/expo
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXX...Never expose CLERK_SECRET_KEY in the client bundle. Next.js and Expo only inline variables prefixed with NEXT_PUBLIC_ or EXPO_PUBLIC_.
Universal Auth - with Clerk
If you merged the plugin from our CLI, then everything should already be set up for you.
But, just to be sure, here’s a quick intro to what that plugin branch does.
Usage of Clerk packages
| Package | Role |
|---|---|
@clerk/nextjs | Next.js app, SSR, API routes |
@clerk/clerk-expo | React Native (Expo) |
@clerk/backend | Server-side auth checks and middleware |
Portable wiring: At each app root, ClerkAuthManager (from @auth/clerk/next or @auth/clerk/expo) installs the right Clerk SDK bundle on ClerkAuthContext, runs SyncClerkUIStatics so compound APIs like UserButton.MenuItems work, and wraps the platform ClerkProvider. Shared types and the runtime bundle shape live in compat.ts.
Feature code should import hooks and UI from @auth/clerk only—do not pass the clerkAuth bundle from a React Server Component into a client component (it is not serializable). See React Portability Patterns and the Clerk provider docs in the sidebar.
The gap between @clerk/nextjs and mobile is closed by portable UI that @clerk/clerk-expo does not ship:
<SignIn /><SignUp /><UserButton /><OrganizationSwitcher />
Differences on Web vs Mobile
We reduced the API surface on mobile; core flows match web, with more configuration on web for now.
See the component pages in the sidebar for universal vs web-only details. For custom UIs: Custom flows — Next.js · Custom flows — Expo .
Next.js - /sign-in, /sign-up
In Next.js, merging this plugin branch adds two new pages to @app/next:
- (auth) / sign-in / [[...rest]] / page.tsx
- (auth) / sign-up / [[...rest]] / page.tsx
Both import from @auth/clerk; on web, SignIn and SignUp are the same components as in @clerk/nextjs—no extra restrictions.
Expo - /sign-in, /sign-up
@clerk/clerk-expo does not ship pre-built auth screens, so this plugin adds portable screens with a smaller API surface.
- (generated) / sign-in / index.tsx
- (generated) / sign-up / index.tsx
- SignInScreen.tsx
- SignUpScreen.tsx
- sign-in / index.native.tsx
- sign-up / index.native.tsx
Implementation references:
| Convention | Meaning |
|---|---|
index.native.tsx | Route re-export consumed only by @app/expo |
(generated)/ | Build script re-exports the route file here |
SignInScreen.tsx / SignUpScreen.tsx implement the screens using <SignIn /> and <SignUp /> from @auth/clerk, which delegate to @clerk/clerk-expo hooks.
Securing universal API calls
Logging in on web, iOS, and Android is not enough—every request to your Next.js backend must carry auth (cookies on web, bearer token on native).
Next.js Middleware
On the web, Clerk’s auth token lives in cookies; clerkMiddleware reads them on each matched request.
This starter uses Next.js 16’s proxy.ts entry (same clerkMiddleware API as the older middleware.ts file). On an older Next major, you may still use middleware.ts and export const middleware—see Clerk’s middleware quickstart .
import { NextResponse } from 'next/server'
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { createMiddlewareContext } from '@green-stack/utils/apiUtils'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/plans(.*)',
// ...
])
// -i- https://nextjs.org/docs/app/api-reference/functions/next-request
export const proxy = clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) await auth.protect()
const authCtx = await auth()
const authToken = await authCtx.getToken()
const headerContext: Partial<RequestContext> = {
userId: authCtx.userId || undefined,
orgId: authCtx.orgId || undefined,
sessionId: authCtx.sessionId || undefined,
authToken: authToken || undefined,
}
return NextResponse.next({
request: {
headers: await createMiddlewareContext(req, headerContext),
},
})
})
export const config = {
matcher: [
'/((?!.+\\.[\\w]+$|_next).*)',
'/',
'/(api|graphql|trpc)(.*)',
],
}The proxy export name and file are framework-specific; the important part is clerkMiddleware so route protection and auth() match what @auth/clerk expects on the client.
Mobile Auth Headers
On mobile there are no cookies—attach the session token to API requests.
The plugin wires getMobileAuthHeaders() into graphqlQuery(), so resolvers and fetchers built via our data resolver pattern stay authenticated on web and mobile.
For one-off REST calls from native:
import { getMobileAuthHeaders } from '@auth/clerk'
const authHeaders = await getMobileAuthHeaders()
// -> { Authorization: 'Bearer …' } on native when signed in
// -> { } when signed out, or on Next.js (cookies handle auth)
fetch('https://your-api-endpoint.com/protected', {
method: 'GET',
headers: {
...authHeaders,
'Content-Type': 'application/json',
},
})Serverside auth checks & utils
Use @clerk/backend server-side to validate tokens. For data resolvers, helpers live under @auth/clerk/utils/server/clerk:
| Export | Purpose |
|---|---|
getResolverClerkClient() | Lazy-import clerkClient so SSR does not throw |
getUser() | User by Clerk userId |
getUserByEmail() | User by email |
getUserOrganisations() | Organizations for a userId |
getAdminOrganisations() | Organizations where the user is an admin |
createOrganisation() | Create an org for userId + name |
getOrganisation() | Org by organizationId |
updateOrganisation() | Patch an org by organizationId |
Example resolver excerpt:
import { getResolverClerkClient } from '@auth/clerk/utils/server/clerk'
/** --- getAuthState() ------------------------------------- */
/** -i- Retrieve the current authentication state */
export const getAuthState = createResolver(async ({ args, context, ... }) => {
// Extract the request context (populated & verified from Next proxy / middleware)
const { userId, orgId, sessionId, authToken } = context
// If anything is missing, the edge layer kept the context empty (= not authenticated)
if (!userId || !sessionId || !authToken) return { isSignedIn: false }
// Get the Clerk client
const clerkClient = await getResolverClerkClient(context)
// Retrieve the session?
const session = await clerkClient.sessions.verifySession(sessionId)
// Retrieve the user object from clerk?
const user = await clerkClient.users.getUser(userId)
// ...
}, getAuthStateBridge)