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
lyricaldevil
lyricaldevil16mo ago
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>
James Perkins
James Perkins16mo ago
This is not how Clerk works really. So
export default authMiddleware({
publicRoutes: ["/"]
});

export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
export default authMiddleware({
publicRoutes: ["/"]
});

export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
This public routes '/" but any tRPC or API route would require you are signed in
lyricaldevil
lyricaldevil16mo ago
That's accurate. the public route is basically just a static landing page that doesn't interact with tRPC or API in any way.
MAST
MAST16mo ago
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.
James Perkins
James Perkins16mo ago
Just set
export default authMiddleware({
publicRoutes: ["/", "/api/(.*)"]
});

export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
export default authMiddleware({
publicRoutes: ["/", "/api/(.*)"]
});

export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
Then handle 401's in tRPC
Want results from more Discord servers?
Add your server