Alt description missing in image
Beta: Plugins coming soon!
@auth/clerkREADME
Alt description missing in image

Universal Auth - Clerk Plugin

Install the with/auth-clerk plugin branch and Pull-Request:

npx @fullproduct/universal-app install with/auth-clerk

This enables universal authentication by adding the @auth/clerk workspace package to your monorepo:

        • <ClerkProvider />
        • <SignUp />
        • <SignIn />
        • <UserButton />
        • <OrganizationSwitcher />
        • useClerk()
        • useAuth()
        • useUser()
        • useSession()
        • useOrganization()
        • useSocialAuthFlow()
        • useSignUpFlow()
        • useSignInFlow()
        • getMobileAuthHeaders()

Requirements - Clerk Setup

This universal auth plugin requires you to have a Clerk account for your project.

You can create one for free at clerk.com/signup.

At the moment of writing these docs, they have a generous free tier:

  • Up to 10k monthly active users
  • Won’t charge you for users who sign up but never return

Clerk Env Vars

To connect your app to Clerk, you’ll need to add some environment variables to your .env files:

apps/expo/.env.local
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXX...

On the Next.js side, we need both the publishable key (for the client) and the secret key (for server-side usage):

apps/nextjs/.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXX...
CLERK_SECRET_KEY=sk_test_XXXXXXX... # Only for server-side usage

Make sure to never publish or expose your CLERK_SECRET_KEY in any way, such as accidentally including it in the client-side bundle. Luckily, next.js and expo both prevent this by only exposing environment variables prefixed with NEXT_PUBLIC_ or EXPO_PUBLIC_

Unversal 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

Our custom @auth/clerk package closes the gaps between Clerk’s own packages for Web, Mobile and the Server:

  • @clerk/nextjs for our Next.js Web App + SSR & API routes
  • @clerk/clerk-expo for our React Native Mobile App
  • @clerk/backend for our Server-side auth checks and middleware

Specifically, the gap between @clerk/nextjs and mobile is closed by recreating similar UI components for Expo, as they’re missing in the @clerk/clerk-expo package:

  • <SignIn />
  • <SignUp />
  • <UserButton />
  • <OrganizationSwitcher />

Differences on Web vs Mobile

We did have to reduce the api surface of these components a bit on mobile, but the core functionality is there. Some options will just be more configurable on web than on mobile (for now). Better this way, than not at all.

In the near future, we already have plans to close the gap even further and keep up with Clerk’s own evolving APIs.

For more details on what works universally, and what doesn’t, check the individual component docs in the sidebar.

Ultimately, you’ll likely want to craft your own custom auth flows and UIs, for which Clerk has it’s own guides:

Next — /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

While both do import from @auth/clerk, the actual SignIn & SignUp implementation on web is exactly the same as in @clerk/nextjs. Meaning there are no restrictions or differences compared to using Clerk directly, e.g:

Expo — /sign-in, /sign-up

As mentioned, the @clerk/clerk-expo package doesn’t provide any pre-built auth screens or UI, so we had to create our own 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

These screens were built by us using Clerk’s guide for creating custom auth flows:

If you’re wondering about the (generated) and index.native.tsx parts in our fs routing setup:

  • index.native.tsx makes sure our scripts only re-export the route to @app/expo
  • (generated) is where our build script re-exports the actual route file to

SignInScreen.tsx and SignUpScreen.tsx hold the actual implementations of the screens for mobile.

  • use our custom <SignIn /> and <SignUp /> components from @auth/clerk
  • … which in turn use the hooks from @clerk/clerk-expo

Cross-platform secure API calls

With this setup, we’re good to log in on all Web, iOS and Android versions of our app.

But signing in alone is not enough. We need to make sure all our requests to our (next.js) back-end are properly authenticated as well.

Next.js Middleware

On the web, Clerk’s auth token is stored through cookies, which we can then read in our next.js middleware:

apps/next/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { createMiddlewareContext } from '@green-stack/utils/apiUtils'
 
/* --- Route protection ----------------- */
 
const isProtectedRoute = createRouteMatcher([
 
    // Screen routes
    '/dashboard(.*)',
    // ...
 
    // API routes
    '/api/users(.*)',
    '/api/plans(.*)',
    // ...
])
 
/* --- Middleware ----------------------- */
 
// -i- https://nextjs.org/docs/app/api-reference/functions/next-request
 
export const middleware = clerkMiddleware(async (auth, req) => {
 
    // Protect routes
    if (isProtectedRoute(req)) await auth.protect()
 
    // Get auth context
    const authCtx = await auth()
    const authToken = await authCtx.getToken()
 
    // Create the request context header (to pass things to API resolvers)
    const headerContext: Partial<RequestContext> = {
        // ...
        userId: authCtx.userId || undefined,
        orgId: authCtx.orgId || undefined,
        sessionId: authCtx.sessionId || undefined,
        authToken: authToken || undefined,
    }
 
    // Execute request handler (and pass the request context header)
    const res = NextResponse.next({
        request: {
            headers: await createMiddlewareContext(req, headerContext),
        },
    })
 
    // Continue by resolving the request
    return res
})
 
/* --- Config --------------------------- */
 
// -i- https://nextjs.org/docs/14/app/building-your-application/routing/middleware
 
export const config = {
    matcher: [
        '/((?!.+\\.[\\w]+$|_next).*)',
        '/',
        '/(api|graphql|trpc)(.*)',
    ],
}

Mobile Auth Headers

On mobile, we don’t have cookies to rely on, so we should include the auth token in the headers of our API calls.

When merging the with/auth-clerk plugin branch, we already added a utility function to our graphqlQuery() helper to do just that:

  • Calls getMobileAuthHeaders() from @auth/clerk to get the auth token from Clerk
  • Includes the auth token in the headers of our request to the next.js back-end

Meaning that all API’s, hooks and fetchers crafted through our recommended way of resolving data are already properly authenticated on both Web and Mobile.

If you need to make custom authenticated requests (e.g. A REST endpoint) from mobile, feel free to use getMobileAuthHeaders() as well:

import { getMobileAuthHeaders } from '@auth/clerk'
 
const authHeaders = await getMobileAuthHeaders()
// -> { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }
// -> { } -- if not signed in, or if used in next.js
 
fetch('https://your-api-endpoint.com/protected', {
    method: 'GET',
    headers: {
        ...authHeaders,
        'Content-Type': 'application/json',
    },
})

Serverside auth checks & utils

You’ll typically use @clerk/backend server-side to check the auth token.

For our way of writing data resolvers, we added some helper functions to simplify.

From @auth/clerk/utils/server/clerk, import:

  • getResolverClerkClient() to lazy import clerkClient so it doesn’t error during SSR
  • getUser() retrieves a user by their Clerk userId
  • getUserByEmail() retrieves a user by their email address
  • getUserOrganisations() takes a userId and returns their organisations
  • getAdminOrganisations() takes a userId and returns the organisations where they’re an admin
  • createOrganisation() creates a new organisation for the given userId + (org) name
  • getOrganisation() retrieves an organisation by its organizationId
  • updateOrganisation() updates an organisation by its organizationId + (obj) updates

Here’s an example of how to extract auth context in a resolver:

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 middleware)
    const { userId, orgId, sessionId, authToken } = context
 
    // If anything is missing, middleware 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)