Pablo
Pablo4d ago

WorkOS action/mutation question

Let's say I have a mutation called "createChannel". A channel belongs to an organization, so it checks for the presence of an organization and there's none. It schedules an action that will create the organization. And you are saying that it should also create the channel? That means all the mutations on my site need to run scheduled just in case.
11 Replies
Clever Tagline
Let's split this off. I'm not familiar with the WorkOS flow, so I'm a little unclear on all the pieces involved.
Pablo
PabloOP4d ago
After logging in, I call await ctx.auth.getUserIdentity() and it returs a user id, and optionally an organization id. If the organization Id is missing, I need to call some APIs in workos to create the organization, and then I need to do a ctx.db.insert to create the matching organization in Convex.
Clever Tagline
Okay. Thanks for that. This goes back to what I originally proposed: * Call a mutation from the app * If the org ID is missing for the user, the mutation schedules an action to create it * That action does its thing and calls another mutation via ctx.runMutation to create the matching org in Convex. I guess I'm not sure how that leads to your assumption that all the mutations from the site need to run scheduled. (to be transparent, I'm far from a Convex expert, so there's a chance that I'm overlooking/missing something)
Pablo
PabloOP4d ago
Some quick pseudo code:
createChannel = mutation(() => {
const org = getOrganization();
if (!org) {
schedule(createOrganization)
}
createChannel({orgId: org.id}); // OOOOPS! org is null
}
createChannel = mutation(() => {
const org = getOrganization();
if (!org) {
schedule(createOrganization)
}
createChannel({orgId: org.id}); // OOOOPS! org is null
}
So, instead it would have to be:
createChannel = mutation(() => {
const org = getOrganization();
if (!org) {
schedule(createOrganizationAndThenCreateChannel)
} else {
createChannel({orgId: org.id});
}
}
createChannel = mutation(() => {
const org = getOrganization();
if (!org) {
schedule(createOrganizationAndThenCreateChannel)
} else {
createChannel({orgId: org.id});
}
}
Ok, I think I found a way to synchronously create the orgs. I'm not sure "how synchronous".
Clever Tagline
Yeah, that sounds about right. I've been hashing over this a bit which is why it's taking so long to get this message out, but here's one approach I'm thinking about:
createChannel = mutation(() => {
const org = getOrganization();
if (!org) {
schedule(createOrgAndChannel)
} else {
schedule(doCreateChannel, {orgId: org.id});
}
}

createOrgAndChannel = internalAction(() => {
const org = createOrgOnWorkOs()
const convexOrg = await runMutation(createConvexOrg, {org})
schedule(doCreateChannel, {orgId: org.id});
})

createConvexOrg = internalMutation(({org}) => {
// Create the Convex version of the org
})

doCreateChannel = internalMutation(({orgId: string}) => {
// Create the channel
})
createChannel = mutation(() => {
const org = getOrganization();
if (!org) {
schedule(createOrgAndChannel)
} else {
schedule(doCreateChannel, {orgId: org.id});
}
}

createOrgAndChannel = internalAction(() => {
const org = createOrgOnWorkOs()
const convexOrg = await runMutation(createConvexOrg, {org})
schedule(doCreateChannel, {orgId: org.id});
})

createConvexOrg = internalMutation(({org}) => {
// Create the Convex version of the org
})

doCreateChannel = internalMutation(({orgId: string}) => {
// Create the channel
})
Not sure if that feels like overkill from your end, but that's where my thoughts are going. I thought some of that might also work with helper functions to reduce the number of mutations, but I couldn't think of a way to make that work.
Pablo
PabloOP4d ago
Yeah, me neither. Composer 1 couldn't either. To me, this is semantically:
org = getOrCreateOrg()
createChannel(org)
org = getOrCreateOrg()
createChannel(org)
If it takes that much extra boilerplate to keep Convex happy, I may switch to something else.
Clever Tagline
Like I said, there may be better ways of approaching the problem. Please don't take my suggestion as the only way to pull it off. I'm still learning a lot about how to optimize processes on Convex. You might also try asking on the #workos channel, as someone there might have a more appropriate solution. My only auth experience is with Clerk, and I don't use the org features.
Pablo
PabloOP4d ago
Thank you @Clever Tagline Oh, I asked a different version of these questions in #workos, but it's a lot more quiet.
Clever Tagline
Maybe open a thread in #support-community and see if that gets any traction? 🤷‍♂️ You could point to this thread for reference on what we've already discussed. I'd love to know if there's a simpler way to solve this. Sorry, this is still eating at me. My brain won't let it go. 😅 Maybe it's my lack of understanding of the relationship between organizations, channels, and users in your use case, and which pieces originate in WorkOS vs your app (Convex). Could you break that down in a basic way?
Pablo
PabloOP4d ago
Users and Channels belong to Organizations. Users and Organizations originate from WorksOS. After log in, there is actually a way to have a callback with WorkOS Authkit and now I'm creating the org there (both in WorkOS and in Convex). And then throughout the rest of the app assuming it exists or crash if it doesn't. So the channel creation looks like this:
export const create = mutation({
args: { },
returns: v.id('channels'),
handler: async (ctx, args): Promise<Id<'channels'>> => {
const { workosOrgId } = await getWorkOsIds(ctx);
const org = await getByWorkosId(ctx, workosOrgId);
return await ctx.db.insert('channels', {
//...
});
},
});
export const create = mutation({
args: { },
returns: v.id('channels'),
handler: async (ctx, args): Promise<Id<'channels'>> => {
const { workosOrgId } = await getWorkOsIds(ctx);
const org = await getByWorkosId(ctx, workosOrgId);
return await ctx.db.insert('channels', {
//...
});
},
});
Clever Tagline
Yeah, that's definitely cleaner than what I proposed. There comes a point where I find myself designing the app such that certain features will only become available under specific conditions; e.g. the feature to create a channel won't even appear unless there's an organization. That forces me to handle the necessary condition (e.g. org creation) somewhere earlier in the flow of onboarding a new user. While I don't have orgs and channels in the app I'm building for work, I've created similar types of failsafes to pretty much force things to be done in a certain sequence.

Did you find this page helpful?