tRPC + Clerk: user & org info

I'm switching from NextAuth to Clerk. With NextAuth, I got the user info like:
import type { Session } from 'next-auth'

type CreateContextOptions = {
session: Session | null
}
import type { Session } from 'next-auth'

type CreateContextOptions = {
session: Session | null
}
With Clerk's getAuth, I only get userId and nothing else about the user. It exists in the SignedInAuthObject type but it's always undefined, so I can't read the user's email or orgSlug in my tRPC context. I can retrieve it like this, but it'd be called too many times and retrieve too much data
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
// Get the session from the server using the getServerSession wrapper function
const session = getAuth(opts.req)
const { userId } = session
if (!userId) return createInnerTRPCContext({ session })

const user = await clerkClient.users.getUser(userId)
const orgs = await clerkClient.users.getOrganizationMembershipList({ userId })

return createInnerTRPCContext({
session,
user,
orgSlug: orgs[0]?.organization?.slug,
})
}
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
// Get the session from the server using the getServerSession wrapper function
const session = getAuth(opts.req)
const { userId } = session
if (!userId) return createInnerTRPCContext({ session })

const user = await clerkClient.users.getUser(userId)
const orgs = await clerkClient.users.getOrganizationMembershipList({ userId })

return createInnerTRPCContext({
session,
user,
orgSlug: orgs[0]?.organization?.slug,
})
}
tRPC setup: t3 then https://clerk.com/docs/nextjs/trpc
TRPC | Clerk
Learn how to integrate Clerk into your Next.js with TRPC
27 Replies
harvey.birdman
harvey.birdman12mo ago
import { prisma } from "~/server/db";

/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = (opts: CreateNextContextOptions) => {
const { req } = opts;
const sesh = getAuth(req);

const userId = sesh.userId;
const orgId = sesh.orgId;

return {
prisma,
currentUser: userId,
currentOrg: orgId,
};
};
import { prisma } from "~/server/db";

/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = (opts: CreateNextContextOptions) => {
const { req } = opts;
const sesh = getAuth(req);

const userId = sesh.userId;
const orgId = sesh.orgId;

return {
prisma,
currentUser: userId,
currentOrg: orgId,
};
};
Works for me
Mocha
Mocha12mo ago
Right, excpet that setAuth(req).orgId === undefined always for me. Same for user?.emailAddresses. Is it defined for you?
harvey.birdman
harvey.birdman12mo ago
Is the user you're logging in with a part of an org and has a slug defined?
export const createTRPCContext = (opts: CreateNextContextOptions) => {
const session = getAuth(opts.req);
const { userId, orgId, orgSlug } = session;
// const sesh = getAuth(req);

console.log("userId", userId);
console.log("orgId", orgId);
console.log("orgSlug", orgSlug);

return {
prisma,
currentUser: userId,
currentOrg: orgId,
};
};

userId user_2NnQUPMwpBPKHbkZM2c7AkhwXnD
orgId org_2OAA34B7ZEoNL2YitGGTmPS86m6
orgSlug polvo
export const createTRPCContext = (opts: CreateNextContextOptions) => {
const session = getAuth(opts.req);
const { userId, orgId, orgSlug } = session;
// const sesh = getAuth(req);

console.log("userId", userId);
console.log("orgId", orgId);
console.log("orgSlug", orgSlug);

return {
prisma,
currentUser: userId,
currentOrg: orgId,
};
};

userId user_2NnQUPMwpBPKHbkZM2c7AkhwXnD
orgId org_2OAA34B7ZEoNL2YitGGTmPS86m6
orgSlug polvo
Mocha
Mocha12mo ago
Yes, I signed in assigned another user to an org just to make sure. Same code prints out:
userId user_2RFzjOPkQkvUKUgXkPXOEYBk0
orgId undefined
orgSlug undefined
userId user_2RFzjOPkQkvUKUgXkPXOEYBk0
orgId undefined
orgSlug undefined
Everything works well when using clerkClient or useSession. Also my Clerk is in test mode if that means anything
harvey.birdman
harvey.birdman12mo ago
Definitely a bit odd if it all looks like it should work. Might be worth asking in the Clerk discord if everything looks good otherwise. Have multiple clerk applications and connecting to the wrong one? Sorry not too much help just saw something i recognized
harvey.birdman
harvey.birdman12mo ago
Here's my full trpc.ts just so you see how it's working for me https://gist.github.com/msywulak/4f5c0f6991dfbcef46d801f316f4f46e
Gist
trpc.ts
trpc.ts. GitHub Gist: instantly share code, notes, and snippets.
Mocha
Mocha12mo ago
Thanks for sharing the gist, highly appreciated! At least now I know it should work so I just figured out the org just wasn't selected. It works when I switch to the org from OrganizationSwitcher But how do I prevent it from being set to personal workspace (if the user is assigned to an org)? It's ok if I can do that from Dashboard too
harvey.birdman
harvey.birdman12mo ago
Hmm I don't have anything set in this app that allows users to switch organizations and I require an org to get to the dashboard. So on initial sign-up I check if there's an org and if not make them create one. But don't have anything that allows them to switch to a personal workspace or change/create orgs I'll play around with it and see if I can reproduce Ok yeah, if I add the org switcher component and switch to personal workspace I start getting the same errors.
❌ tRPC failed on appPermissions.getByUserId: No org
orgId undefined
orgSlug undefined
❌ tRPC failed on appPermissions.getByUserId: No org
orgId undefined
orgSlug undefined
Mocha
Mocha12mo ago
Yes, my goals is that my users don't need to be aware of Workspaces. Maybe at login, auto-assign (?) the user to the first org, if any. I'm using this for access control, so if you don't have orgSlug, you won't get access to the DB
harvey.birdman
harvey.birdman12mo ago
Right, on initial sign-up if there's no org I send them to here, and protect the dashboard by checking for org there too and send them back to make it if they haven't already. Never hit your issue since I guess if you do it this way they're always apart of the org workspace and not a personal one.
import { CreateOrganization } from "@clerk/nextjs";

export default function CreateOrganizationPage() {
return (
<div className="flex h-screen items-center justify-center">
<CreateOrganization afterCreateOrganizationUrl="/dashboard" />;
</div>
);
}
import { CreateOrganization } from "@clerk/nextjs";

export default function CreateOrganizationPage() {
return (
<div className="flex h-screen items-center justify-center">
<CreateOrganization afterCreateOrganizationUrl="/dashboard" />;
</div>
);
}
Though what you initially did looks like what's recommended by Clerk from today even. https://discord.com/channels/856971667393609759/1118957594816565258/1118962394937438228
James Perkins
James Perkins12mo ago
Hey there! So two things: If you want the user emails in getAuth you need to attach it to the session token. We don't provide the entire user for obvious reasons although the we do have a type side effect that shows the user object as an option. Then the organization claims won't be filed in till the user has actively selected their organization. If you are creating them for the user and assigning them, you'll need to use the client hook to assign it, then when it's assigned it will be populated in the claims.
harvey.birdman
harvey.birdman12mo ago
Ahh cool cool so something like this would work but probably want to think about how users with multiple orgs would work.
type OrganizationsType = {
[key: string]: string;
};

export const createTRPCContext = (opts: CreateNextContextOptions) => {
const session = getAuth(opts.req);
const { userId, orgId, sessionClaims } = session;

const organizations =
(sessionClaims?.organizations as OrganizationsType) || [];
const organizationKeys = Object.keys(organizations);
console.log("orgnaizationKeys", organizationKeys);

return {
prisma,
currentUser: userId,
currentOrg: orgId ?? organizationKeys[0],
};
};
type OrganizationsType = {
[key: string]: string;
};

export const createTRPCContext = (opts: CreateNextContextOptions) => {
const session = getAuth(opts.req);
const { userId, orgId, sessionClaims } = session;

const organizations =
(sessionClaims?.organizations as OrganizationsType) || [];
const organizationKeys = Object.keys(organizations);
console.log("orgnaizationKeys", organizationKeys);

return {
prisma,
currentUser: userId,
currentOrg: orgId ?? organizationKeys[0],
};
};
Mocha
Mocha12mo ago
Thanks @jamesperkins @harvey.birdman ! Appreciate your help I talked with the Clerk team, and that's sadly not possible even with token customization. It still won't show the org if it's not selected I guess my tRPC question now is how can I avoid having to make remote calls every single time the user opens a new page? createTRPCContext gets called on every route update
James Perkins
James Perkins12mo ago
sessionClaims isn't a network call, its a decode. so their is zero remote requests invovled. unless I don't understand your question
Mocha
Mocha12mo ago
Oh yes, I tried and modified sessionClaims to include things like email, but I still couldn't get it to include the org. It only shows the org if the user has manually selected it.
James Perkins
James Perkins12mo ago
Correct. the user has to be attached tot heir current active org.
Mocha
Mocha12mo ago
And I don't want the user to manually select it
James Perkins
James Perkins12mo ago
So just run a useEffect to have them attachded to it.
Mocha
Mocha12mo ago
How'd I do that? The Clerk team told me they may release a future update for org-only customers (which is my use case)
James Perkins
James Perkins12mo ago
something like
export function SyncActiveOrg() {
const {orgId} = useAuth();
const { setActive, organizationList, isLoaded } = useOrganizationList();

React.useEffect(() => {
if (!isLoaded) return;

if (orgId) {
return;
}

const org = organizationList?[0].id

if (org) {
void setActive(org);
}
}, [isLoaded]);

return null;
}
export function SyncActiveOrg() {
const {orgId} = useAuth();
const { setActive, organizationList, isLoaded } = useOrganizationList();

React.useEffect(() => {
if (!isLoaded) return;

if (orgId) {
return;
}

const org = organizationList?[0].id

if (org) {
void setActive(org);
}
}, [isLoaded]);

return null;
}
FYI I work at Clerk. 🙂
Mocha
Mocha12mo ago
Oh hi! 🙂
James Perkins
James Perkins12mo ago
We do have plans for auto join and potentially serverside activation
Mocha
Mocha12mo ago
I like everything about Clerk except this one thing, and I try to avoid workarounds I wish it was as easy as Auth.js
James Perkins
James Perkins12mo ago
Yeah we made this descion because our organizations are built around the notion or vercel, where 1 user can belong to 10000 orgs. Once they are active we don't forget but we have to have someone tell us which one should be the correct one.
Mocha
Mocha12mo ago
Yes that works well for very-SaaS apps like Notion. Ours is more enterprise-y where everyone must always belong to exactly 1 org Personal workspace / multiple orgs don't make any sense for apps like ours Where would you call syncActiveOrg() in this case?
James Perkins
James Perkins12mo ago
somewhere like after sign in or after sign up to double check. they are in it. FWIW that code is very much untested I wrote it on my phone.
Mocha
Mocha12mo ago
Thx! I'll give it a try