Implementing Authorization with Clerk in a tRPC app running on a Cloudflare Worker
Tags: serverless, tutorial, node, api
Published: 2023-09-17 12:04:55

While integrating integrating Clerk into a tRPC app on NextJS is documented in the guide [Using tRPC and Next.js](https://clerk.com/docs/references/nextjs/trpc). I wanted to deploy my tRPC server as a standalone service. So the API could be used for non-browser clients, in my case, a React Native app. I opted to use Cloudflare Workers.

I was able to get the tRPC app up and running, but I ran into some issues with the Clerk integration. I was able to get it working, but I had to do some digging to figure out how to get it working. I figured I'd share what I learned.

## Prerequisites

- Basic knowledge of tRPC is assumed. If you're not familiar with tRPC, I recommend checking out [tRPC docs](https://trpc.io/docs/quickstart)
- A [Clerk](https://clerk.com) account. You'll need to create a Clerk app and get your Clerk API key.
- A Cloudflare account to deploy the API.

## Authorization Strategy

To integrate Clerk into tRPC, we'll use the following authorization strategy:

1. The user authenticates on the frontend and receives a [Clerk session Token](https://clerk.com/docs/backend-requests/resources/session-tokens).
2. User sends the session token along with every request to the tRPC API through the Authorization header.
3. tRPC validates the session token and adds the user data to the tRPC context if the token is valid or null if the token is invalid.
4. A tRPC auth middleware checks if the user data is in the tRPC context. If it is, the user is authenticated and the request is allowed to continue. If not, the user is not authenticated and the request is rejected.
5. A `protectedProcedure` is created which uses the tRPC auth middleware to protect the procedure. If the user is authenticated, the procedure is allowed to continue. If not, the procedure is rejected.

## Set up tRPC

After your project is initialized, you'll need to install the tRPC server package and `zod` for validation. Navigate to your project directory and run the following command:

### Install tRPC

```bash
npm install @trpc/server zod
```

Next, create a `trpc.ts` file in the `src` directory. This is where we'll set up our tRPC server and context. Add the following code to the file:

```typescript
import { initTRPC, inferAsyncReturnType } from "@trpc/server";
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";

// context holds data that all of your tRPC procedures will have access to
export const createContext = async (options: FetchCreateContextFnOptions) => {
  return {};
};

// initialize tRPC
const t = initTRPC.context<typeof createContext>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
```

### Create a tRPC router

Next, create a `router.ts` file in the `src` directory. This is where we'll define our tRPC routers. Add the following code to the file:

```typescript
import { z } from "zod";
import { router, publicProcedure } from "./trpc";

// example router
export const exampleRouter = router({
  sayHello: publicProcedure
    .input(
      z.object({
        name: z.string(),
      })
    )
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.name}`,
      };
    }),
});

// app router
export const appRouter = router({
  example: exampleRouter,
});

export type AppRouter = typeof appRouter;
```

### Create a Cloudflare Worker project

You can initialize a new Cloudflare Worker project by running the `npm create cloudflare@2 -- <project-name>` command in your terminal and answering the prompts that follow. I named my project `trpc-clerk-cloudflare-worker`. Choose the `hello-world` template when prompted. In my case, I also chose to use TypeScript.

```bash
npm create cloudflare@2 -- trpc-clerk-cloudflare-worker
```

### Startup the tRPC server with Cloudflare Worker

Next, navigate to the entry point of your project, which is located at `src/index.ts` by default. Add the following code to the file:

```typescript
import { appRouter } from "./router";
import { createContext } from "./trpc";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

export default {
  async fetch(request: Request): Promise<Response> {
    const response = await fetchRequestHandler({
      req: request,
      createContext,
      endpoint: "/trpc",
      router: appRouter,
      onError({ error, path }) {
        console.error(`tRPC Error on '${path}'`, error);
      },
    });

    return response;
  },
};
```

### Handle CORS and CORS preflight requests

Lastly, we need to handle CORS and CORS preflight requests. Add the following code to the `src/index.ts` file:

```typescript
import { appRouter } from "./router";
import { createContext } from "./trpc";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method === "OPTIONS") {
      return handleCORSPreflight();
    }

    const response = await fetchRequestHandler({
      req: request,
      createContext,
      endpoint: "/trpc",
      router: appRouter,
      onError({ error, path }) {
        console.error(`tRPC Error on '${path}'`, error);
      },
    });

    return addCORSHeaders(response);
  },
};

const addCORSHeaders = (res: Response) => {
  const response = new Response(res.body, res);
  response.headers.set("Access-Control-Allow-Origin", "*");
  response.headers.set("Access-Control-Allow-Headers", "*");
  response.headers.set("Access-Control-Allow-Credentials", "true");
  response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");

  return response;
};

const handleCORSPreflight = () => {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Credentials": "true",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    },
  });
};
```

To test that everything is working, run `npm run start` in your terminal. If everything goes well, you should see an output similar to this:

```bash
$ wrangler dev
⛅️ wrangler 3.8.0
------------------

.....

⎔ Starting local server...
[mf:inf] Ready on http://0.0.0.0:xxxxx
```

You can now test your tRPC server by making a request to `http://localhost:xxxxx/trpc`. You should see a response similar to this:

```json
{
  "error": {
    "message": "No "query"-procedure on path",
    "code": -32004,
    "data": {
      "code": "NOT_FOUND",
      "httpStatus": 404,
      "stack": "...blah...blah",
      "path": ""
    }
  }
}
```

## Integrate Clerk with tRPC

### Install Clerk Backend SDK

Next, install the Clerk backend SDK package by running the following command in your terminal:

```bash
npm install @clerk/backend
```

### Set environment secrets

Go to your Clerk dashboard and create a new Clerk app. Once your app is created, you'll need to get your Clerk API key. We'll utilize Cloudflare [Workers Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) to securely store our Clerk API key. To do this, create a `.dev.vars` file in the root of your project and add the API key to the file like this:

```bash
CLERK_API_KEY=<your-clerk-api-key>
```

Go to your Cloudflare worker entry point, `src/index.ts`, and modify the `fetch` function to look like this:

```typescript
// ...imports

export interface Env {
  CLERK_API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === "OPTIONS") {
      return handleCORSPreflight();
    }

    const headers = new Headers();

    const response = await fetchRequestHandler({
      req: request,
      //   env is passed to the createContext function
      createContext: () =>
        createContext({ env: env, req: request, resHeaders: headers }),
      endpoint: "/trpc",
      router: appRouter,
      onError({ error, path }) {
        console.error(`tRPC Error on '${path}'`, error);
      },
    });

    return addCORSHeaders(response);
  },
};

// ...rest of the code
```

### Using Clerk in your tRPC Context

Navigate to `trpc.ts` where we created a `createContext` function earlier. We'll need to update the `createContext` function to use the Clerk backend SDK to validate the session token and add the user session data to the tRPC context. We also need to modify the `createContext` function to accept an `env` parameter. Add the following code to the `trpc.ts` file:

```typescript
// import the Env interface
import { Env } from ".";
import { Clerk } from "@clerk/backend";
import { initTRPC, inferAsyncReturnType, TRPCError } from "@trpc/server";
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";

// update the createContext function to accept an env parameter
interface Context extends FetchCreateContextFnOptions {
  env: Env;
}

// context holds data that all of your tRPC procedures will have access to
export const createContext = async ({ req, env }: Context) => {
  const clerk = Clerk({ apiKey: env.CLERK_API_KEY });
  const userId = req.headers.get("authorization");

  //If there is no session token, return null
  if (!userId) return { session: null };

  // otherwise, get the session
  const user = await clerk.users.getUser(userId);
  return {
    user,
  };
};

// ...rest of the code
```

### Create a tRPC auth middleware

The next step is to create a tRPC auth middleware. This middleware will check if the user session data is in the tRPC context. We'll also create a `protectedProcedure` which uses the tRPC auth middleware. We can then use the `protectedProcedure` to protect any procedure we want to require authentication. Add the following code to the `trpc.ts` file:

```typescript
// ... rest of the code

const isAuthenticated = t.middleware(({ ctx, next }) => {
  if (!ctx.user?.id) throw new TRPCError({ code: "UNAUTHORIZED" });

  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const protectedProcedure = t.procedure.use(isAuthenticated);
```

### Create a route that requires authentication

Now that we have our auth middleware, we can create a route that requires authentication. Add the following code to the `router.ts` file:

```typescript
// Example protected procedure
export const exampleProtectedRouter = router({
  // this is accessible only if the user is authenticated
  getSecret: protectedProcedure.query(({ ctx }) => {
    return {
      secret: `User ID: ${ctx.user.id}`,
    };
  }),
});

export const appRouter = router({
  example: exampleRouter,
  // add new router to app router
  protectedExample: exampleProtectedRouter,
});
```

## Deploy your tRPC API

The next step is to deploy your tRPC API. Before we do that, we need to add our Clerk API key to our Cloudflare Worker environment. To do this, run `npx wrangler secret put CLERK_API_KEY` in your terminal, you'll be prompted to enter your Clerk API key. Once you've added your Clerk API key, you can deploy your tRPC API by running `npm run deploy` in your terminal. If everything goes well, you should see an output similar to this:

```bash
> trpc-clerk-cloudflare-worker@0.0.0 deploy
> wrangler deploy

 ⛅️ wrangler 3.8.0
------------------
Total Upload: 257.98 KiB / gzip: 50.85 KiB
Uploaded trpc-clerk-cloudflare-worker (2.75 sec)
Published trpc-clerk-cloudflare-worker (3.32 sec)
https://trpc-clerk-cloudflare-worker.xxx.workers.dev
Current Deployment ID: xxxx-8443-xxx-b0a6-37xxxx0d4b5f13
```

## Testing everything out

To make sure everything works as expected, we'll create a simple tRPC NextJS frontend app that uses the tRPC API we just created. We'll use the [Clerk's Next.js SDK](https://clerk.com/docs/references/nextjs/overview) to authenticate the user and get the session token. We'll then use the session token to make a request to the tRPC API.

Since one of the main advantages of tRPC is to share types between the frontend and backend, we'll create a parent folder that will hold both the frontend and backend code. We'll call this folder `trpc-clerk`.

### Create a NextJS app

Create a new NextJS app by running `npx create-next-app@latest` in your terminal and answering the prompts that follow. I named my app `trpc-clerk-nextjs` and also chose to use the App router.

Our folder structure should now look like this:

```bash
trpc-clerk
│
├── trpc-clerk-nextjs
│
└── trpc-clerk-cloudflare-worker
```

### Set Up Clerk In Your NextJS App

Follow [Clerk's NextJS Guide](https://clerk.com/docs/quickstarts/nextjs) to set up Clerk in your NextJS app. Once you've completed the guide, you should have a working NextJS app with Clerk authentication.

### Setup tRPC in your NextJS app

You'll need to install the tRPC client package and its dependencies. Navigate to your NextJS app directory and run the following command:

```bash
npm install @trpc/client @trpc/server @trpc/react-query @tanstack/react-query @tanstack/react-query-devtools @tanstack/react-query-next-experimental zod
```

### Create tRPC hooks

Next, we need to create our strongly-typed React hooks using the `createTRPCReact` function from `@trpc/react-query` and our AppRouter type definition from our tRPC API. Create a `trpc.ts` file in the `app` directory of our NextJS app and add the following code to the file:

```typescript
import { createTRPCReact } from "@trpc/react-query";
// import our AppRouter type definition
import type { AppRouter } from "../../trpc-clerk-cloudflare-worker/src/router";

export const trpc = createTRPCReact<AppRouter>();
```

### Setup tRPC Provider

First, we add the URL of your tRPC API to your `.env.local` file: `NEXT_PUBLIC_API_URL=<your-trpc-api-url>`. Next, we need to create a tRPC client and wrap our NextJS app in a tRPC provider. To do that, create a `provider.tsx`file in the`app` directory of our NextJS app and add the following code to the file:

```typescript
"use client";
import React from "react";
import { trpc } from "./trpc";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";

interface Props {
  headers: Headers;
  userId: string | null;
  children: React.ReactNode;
}

const API_URL = const API_URL = process.env.NEXT_PUBLIC_API_URL;

export const AppProvider: React.FC<Props> = (props) => {
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 5 * 1000,
          },
        },
      })
  );

  const [trpcClient] = React.useState(() =>
    trpc.createClient({
      links: [
        loggerLink({
          enabled: (opts) =>
            process.env.NODE_ENV === "development" ||
            (opts.direction === "down" && opts.result instanceof Error),
        }),
        unstable_httpBatchStreamLink({
          url: `${API_URL}/trpc`,
          headers() {
            const headers = new Map(props.headers);
            // ypu can set any http headers you want here
            headers.set("x-trpc-source", "nextjs");
            headers.set("authorization", `${props.userId}`);
            return Object.fromEntries(headers);
          },
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <ReactQueryStreamedHydration>
          {props.children}
        </ReactQueryStreamedHydration>
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </trpc.Provider>
  );
};
```

### Wrap your NextJS app in the tRPC provider

Now that we have our tRPC provider, we need to wrap our NextJS app in the provider, pass in the `userId` and `headers` props, and pass the props to the `AppProvider` component. To do this, open the `layout.tsx` file in the `app` directory of our NextJS app and add the following code to the file:

```typescript
import "@/app/globals.css";
import { headers } from "next/headers";
import { AppProvider } from "@/app/provider";
import { ClerkProvider, auth } from "@clerk/nextjs";

interface Props {
  children: React.ReactNode;
}

export default async function RootLayout({ children }: Props) {
  const { userId } = auth();

  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <AppProvider headers={headers()} userId={userId}>
            {children}
          </AppProvider>
        </body>
      </html>
    </ClerkProvider>
  );
}
```

### Make a request to your tRPC API

Finally! We're ready to make a request to our tRPC API. To do this, open the `page.tsx` file in the `app` directory of our NextJS app and add the following code to the file:

```typescript
"use client";
import { trpc } from "./trpc";
import { UserButton } from "@clerk/nextjs";

export default function Home() {
  const greet = trpc.example.sayHello.useQuery(
    {
      name: "John Doe",
    },
    {
      enabled: false,
    }
  );

  const getProtectedSecret = trpc.protectedExample.getSecret.useQuery(
    undefined,
    {
      enabled: false,
    }
  );

  return (
    <div>
      <UserButton afterSignOutUrl="/" />

      <div>
        <div>
          <div>
            {getProtectedSecret.data
              ? getProtectedSecret.data?.secret
              : "************"}
          </div>
          <button onClick={() => getProtectedSecret.refetch()}>
            {getProtectedSecret.isFetching ? "Loading..." : "Get Secret"}
          </button>
        </div>

        <div>
          <div>{greet.data ? greet.data?.greeting : "************"}</div>
          <button onClick={() => greet.refetch()}>
            {greet.isFetching ? "Loading..." : "Say Hello"}
          </button>
        </div>
      </div>
    </div>
  );
}
```

### Wrapping up

That's it! You should now have a working tRPC API that uses Clerk for authentication. You can find the complete code for this tutorial [here](https://github.com/yinkakun/trpc-clerk)

## Resources

- [tRPC Authorization](https://trpc.io/docs/server/authorization)
- [Clerk Backend SDK](https://clerk.com/docs/references/backend/overview)
- [Clerk Session Tokens](https://clerk.com/docs/backend-requests/resources/session-tokens)
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
- [Cloudflare Secrets](https://developers.cloudflare.com/workers/configuration/secrets/#add-secrets-to-your-project)
- [tRPC + Cloudflare Workers Example](https://github.com/trpc/trpc/tree/main/examples/cloudflare-workers)