More Clerk + tRPC weirdness
I've seen a few questions recently related to the interaction between Clerk and tRPC, but my issue is apparently just different enough that it's not solved by the same things. I'm attempting to make the root route ("/") public and everything else protected, but any tRPC calls return a 401 error. For reference:
// src/middleware.ts
export default authMiddleware({
publicRoutes: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
// src/middleware.ts
export default authMiddleware({
publicRoutes: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
// src/server/api/trpc.ts
const createInnerTRPCContext = ({ auth }: AuthContext) => {
return {
auth,
prisma,
};
};
export const createTRPCContext = async ({
req,
res,
}: CreateNextContextOptions) => {
return createInnerTRPCContext({
auth: getAuth(req),
});
};
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.auth.userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
auth: ctx.auth,
},
});
});
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
// src/server/api/trpc.ts
const createInnerTRPCContext = ({ auth }: AuthContext) => {
return {
auth,
prisma,
};
};
export const createTRPCContext = async ({
req,
res,
}: CreateNextContextOptions) => {
return createInnerTRPCContext({
auth: getAuth(req),
});
};
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.auth.userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
auth: ctx.auth,
},
});
});
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
5 Replies
And here's the response body of an example tRPC request:
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
window.__clerk_frontend_api = '<REMOVED>.clerk.accounts.dev';
window.__clerk_debug = {"frontendApi":"<REMOVED>.clerk.accounts.dev","isSignedIn":false,"proxyUrl":"","isInterstitial":true,"reason":"cross-origin-referrer","message":"","publishableKey":"<REMOVED>","isSatellite":false,"domain":""};
window.startClerk = async () => {
function formRedirect(){
const form = '<form method="get" action="" name="redirect"></form>';
document.body.innerHTML = document.body.innerHTML + form;
const searchParams = new URLSearchParams(window.location.search);
for (let paramTuple of searchParams) {
const input = document.createElement("input");
input.type = "hidden";
input.name = paramTuple[0];
input.value = paramTuple[1];
document.forms.redirect.appendChild(input);
}
const url = new URL(window.location.origin + window.location.pathname + window.location.hash);
window.history.pushState({}, '', url);
document.forms.redirect.action = window.location.pathname + window.location.hash;
document.forms.redirect.submit();
}
const Clerk = window.Clerk;
try {
await Clerk.load({
isSatellite: false,
signInUrl: undefined
});
if(Clerk.loaded){
if(window.location.href.indexOf("#") === -1){
window.location.href = window.location.href;
} else if (window.navigator.userAgent.toLowerCase().includes("firefox/")){
formRedirect();
} else {
window.location.reload();
}
}
} catch (err) {
console.error('Clerk: ', err);
}
};
(() => {
const script = document.createElement('script');
script.setAttribute('data-clerk-publishable-key', '<REMOVED>');
;
script.async = true;
script.src = 'https://<REMOVED>.clerk.accounts.dev/npm/@clerk/clerk-js@latest/dist/clerk.browser.js';
script.crossOrigin = 'anonymous';
script.addEventListener('load', startClerk);
document.body.appendChild(script);
})();
</script>
</body>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
window.__clerk_frontend_api = '<REMOVED>.clerk.accounts.dev';
window.__clerk_debug = {"frontendApi":"<REMOVED>.clerk.accounts.dev","isSignedIn":false,"proxyUrl":"","isInterstitial":true,"reason":"cross-origin-referrer","message":"","publishableKey":"<REMOVED>","isSatellite":false,"domain":""};
window.startClerk = async () => {
function formRedirect(){
const form = '<form method="get" action="" name="redirect"></form>';
document.body.innerHTML = document.body.innerHTML + form;
const searchParams = new URLSearchParams(window.location.search);
for (let paramTuple of searchParams) {
const input = document.createElement("input");
input.type = "hidden";
input.name = paramTuple[0];
input.value = paramTuple[1];
document.forms.redirect.appendChild(input);
}
const url = new URL(window.location.origin + window.location.pathname + window.location.hash);
window.history.pushState({}, '', url);
document.forms.redirect.action = window.location.pathname + window.location.hash;
document.forms.redirect.submit();
}
const Clerk = window.Clerk;
try {
await Clerk.load({
isSatellite: false,
signInUrl: undefined
});
if(Clerk.loaded){
if(window.location.href.indexOf("#") === -1){
window.location.href = window.location.href;
} else if (window.navigator.userAgent.toLowerCase().includes("firefox/")){
formRedirect();
} else {
window.location.reload();
}
}
} catch (err) {
console.error('Clerk: ', err);
}
};
(() => {
const script = document.createElement('script');
script.setAttribute('data-clerk-publishable-key', '<REMOVED>');
;
script.async = true;
script.src = 'https://<REMOVED>.clerk.accounts.dev/npm/@clerk/clerk-js@latest/dist/clerk.browser.js';
script.crossOrigin = 'anonymous';
script.addEventListener('load', startClerk);
document.body.appendChild(script);
})();
</script>
</body>
This is not how Clerk works really.
So
This public routes '/"
but any tRPC or API route would require you are signed in
export default authMiddleware({
publicRoutes: ["/"]
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
export default authMiddleware({
publicRoutes: ["/"]
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
That's accurate. the public route is basically just a static landing page that doesn't interact with tRPC or API in any way.
There's another use as well.
I some endpoints to be public endpoints that user can call from the front-end without being logged in. But Clerk is stopping all requests to tRPC.
Don't know how to handle that one.
Just set
Then handle 401's in tRPC
export default authMiddleware({
publicRoutes: ["/", "/api/(.*)"]
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
export default authMiddleware({
publicRoutes: ["/", "/api/(.*)"]
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};