While integrating integrating Clerk into a tRPC app on NextJS is documented in the guide Using tRPC and Next.js. 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
- A Clerk 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:
- The user authenticates on the frontend and receives a Clerk session Token.
- User sends the session token along with every request to the tRPC API through the Authorization header.
- 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.
- 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.
- 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
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:
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:
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.
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:
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:
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:
$ 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:
{
"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:
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 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:
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:
// ...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:
// 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:
// ... 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:
// 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:
> 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 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:
trpc-clerk
│
├── trpc-clerk-nextjs
│
└── trpc-clerk-cloudflare-worker
Set Up Clerk In Your NextJS App
Follow Clerk's NextJS Guide 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:
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:
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 theapp
directory of our NextJS app and add the following code to the file:
"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:
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:
"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
Top comments (4)
I'm no security expert, but this doesn't seem to be too secure since user_id can be obtain by third-party or, even worse, exposed to the client and I don't see any token validation here, so if you have the user_id you could do anything this user can via the API, right?
I think better way to handle this would be to pass the token and sessionId from the app to the api endpoint via headers and verify it like that:
I think you're right! Thank you so much, I'll update the article accordingly.
any auth0 implemetation or how to do so ?