Create a User model in Prisma with Clerk on Signup

One of the benefits I find with NextAuth is that it simplifies a lot by adding a User model to Prisma, so that I have each user in my database right at the start. I'm having some difficulties in finding some documentation to implement this with Clerk. I guess that I need to create some kind of mutation right when the user signs up. But in that case I would need the data (atleast userId) right when the user creates their account (I am using Discord Provider). Anyone know of some kind of solution to implement this? Do I perhaps need to create some custom authentication with Clerk?
43 Replies
James Perkins
James Perkins15mo ago
So by default you don't need a users table. You just need the user_id. However in cases where you might need it this is made possible by either webhooks or handling it clientside after sign up.
James Perkins
James Perkins15mo ago
webhook example attached
James Perkins
James Perkins15mo ago
Basically Clerk is the users table, so maximum you need user_id for relationships which is avail anywhere in the stack (client,middleware,server) so it's always available for any DB calls for relationships.
fjzon
fjzon15mo ago
Sorry for my lack of knowledge. I've managed to handle like "the current sessioned user". But I am currently building a golf application. So when the user creates a new Round, she/he should be able to add other users to play with. I am completely with you that it's enough with the user_id. But I think that each user should live somewhere in my database, to handle relations between the users. Therefor I thought that I would need to search for all the other users from the database whenever I create a new round (like api.users.getAll.useQuery()).
James Perkins
James Perkins15mo ago
Sure so I don't know the full business case... Seems unscalable to produce every user you have in the app to be returned at once 100,000 users is a lot to be retrieving for a single lookup. Where you have the bare minimum table called golfers for example with something like : user_id name email. (maybe) Enough to build the relationship. That is exactly what is in the webhook provided above. Or if it's more social where it's a golfer with a friend list where they can invite them to a round for example. Then you only need the user_id's in your DB because you can call Clerk to get all the user data directly like names, images, etc.
fjzon
fjzon15mo ago
Alright cool! Ye the fetch all users was just some random example. It's more like you wrote that each user invites their friends. And yes, if I atleast am able to store the user_id in the database for each user, i'm satisfied! Im not quite familiar with Webhooks. But I guess I could find some documentation in how I would implement the code you provided. Thank you for that! I find Clerk really cool by the way, so there's no like "offense" saying what NextAuth provides. It's just what I'm used to and learned to know.
James Perkins
James Perkins15mo ago
Oh none taken! Next Auth is awesome and still my go to for self hosting when Clerk doesn't meet the requirements. It's hard to wrap your mental model around Clerk when you are using self hosting stuff because you need tables etc. The webhook I provided is a great start point but you can read more about how this all works here: https://clerk.com/docs/users/sync-data-to-your-backend
Jazon
Jazon15mo ago
Realized that I will also need to use webhooks for something similar. I got a request too work if I put a webhook.js file in nextjs/pages/api-folder. However, if I put it in like expo/src/api, it doesn't work. I assume I will have to run the tunnel (localTunnel) from the expo folder. And run it on port 19000, since that's what it looks like expo is running from.
James Perkins
James Perkins15mo ago
Well expo doesn’t have an server side api endpoint like next.js. Which is why Next.js works and Expo doesn’t. Why would you need a webhook for expo? If your thinking about Clerk in this scenario. Both Next.js and Expo sign ups work the same so the webhook endpoint can live anywhere (next.js) and we will handle all sign ups
jingleberry
jingleberry15mo ago
Possibly related, for super simple use cases is clerk user metadata sufficient? For example I want to keep track of a stripe customer Id mapped to its clerk user id. Originally I thought of saving the mapping in db. But then I realised it’s much simpler to just have the stripe customer Id saved as clerk user metadata (and you can do the same in stripe) Will I face any pitfalls with this approach? (i.e is it risky to store important info using the clerk user metadata api?)
albatroz
albatroz15mo ago
how can i add this as a router? basically i am asking where to add this function?
fjzon
fjzon15mo ago
@Albatros So from what I understand of what James said. Both the NextJS and Expo apps are connected to the same Clerk API (or whatever you should say). Basically that means that whenever an event is fired from NextJS or Expo that has something to do with Clerk, it affects the same data. So what you can do is that you should be able to put a file called webhook.ts/js in the NextJS api folder, since that's the only api endpoint that acts on the server. E.g. your-application/apps/nextjs/src/pages/api/webhook.ts/js Remember that when you use this Webhooks locally, you need something like localTunnel or Ngrok to make it work. I watched this video yesterday: https://www.youtube.com/watch?v=rAv4ZLGM22Q&ab_channel=JamesPerkins
James Perkins
James Perkins15mo ago
Ah sweet you watched my video 😂
albatroz
albatroz15mo ago
You meanE.g. your-application/apps/nextjs/src/pages/api/webhook/index.js ??
James Perkins
James Perkins15mo ago
That is up to you, either is acceptable, if you want to use a folder structure that works also
albatroz
albatroz15mo ago
I am not sure what i am doing wrong. i added env variable to vercel deployment. I can also see the webhook function being called by clerk. But clerk says it it's 404
albatroz
albatroz15mo ago
albatroz
albatroz15mo ago
James Perkins
James Perkins15mo ago
What is the URL that you setup in Clerk.
albatroz
albatroz15mo ago
import type { IncomingHttpHeaders } from "http";
import type { NextApiRequest, NextApiResponse } from "next";
import type { WebhookRequiredHeaders } from "svix";
import { Webhook } from "svix";
import { Prisma, prisma } from "@acme/db";

const webhookSecret: string = process.env.WEBHOOK_SECRET || "";

export default async function handler(
req: NextApiRequestWithSvixRequiredHeaders,
res: NextApiResponse,
) {
const payload = JSON.stringify(req.body);
const headers = req.headers;
const wh = new Webhook(webhookSecret);
let evt: Event | null = null;
try {
evt = wh.verify(payload, headers) as Event;
} catch (_) {
return res.status(400).json({});
}

const { id } = evt.data;
// Handle the webhook
const eventType: EventType = evt.type;

if (eventType === "user.created" || eventType === "user.updated") {
const { gstNumber, phoneNumber, id, name } = evt.data;

if (!phoneNumber) {
return res.status(400).json({});
}

await prisma.user.upsert({
where: { id: id },
update: {
name: name,
gstNumber: gstNumber,
},
create: {
id: id,
phoneNumber: phoneNumber,
gstNumber: "",
role: "BASIC",
subscribedPlans: { create: { enabledPlan: "BASIC" } },
},
});
}
console.log(`User ${id} was ${eventType}`);
res.status(201).json({});
}

type NextApiRequestWithSvixRequiredHeaders = NextApiRequest & {
headers: IncomingHttpHeaders & WebhookRequiredHeaders;
};

type Event = {
data: Prisma.UserCreateInput;
object: "event";
type: EventType;
};

type EventType = "user.created" | "user.updated" | "*";
import type { IncomingHttpHeaders } from "http";
import type { NextApiRequest, NextApiResponse } from "next";
import type { WebhookRequiredHeaders } from "svix";
import { Webhook } from "svix";
import { Prisma, prisma } from "@acme/db";

const webhookSecret: string = process.env.WEBHOOK_SECRET || "";

export default async function handler(
req: NextApiRequestWithSvixRequiredHeaders,
res: NextApiResponse,
) {
const payload = JSON.stringify(req.body);
const headers = req.headers;
const wh = new Webhook(webhookSecret);
let evt: Event | null = null;
try {
evt = wh.verify(payload, headers) as Event;
} catch (_) {
return res.status(400).json({});
}

const { id } = evt.data;
// Handle the webhook
const eventType: EventType = evt.type;

if (eventType === "user.created" || eventType === "user.updated") {
const { gstNumber, phoneNumber, id, name } = evt.data;

if (!phoneNumber) {
return res.status(400).json({});
}

await prisma.user.upsert({
where: { id: id },
update: {
name: name,
gstNumber: gstNumber,
},
create: {
id: id,
phoneNumber: phoneNumber,
gstNumber: "",
role: "BASIC",
subscribedPlans: { create: { enabledPlan: "BASIC" } },
},
});
}
console.log(`User ${id} was ${eventType}`);
res.status(201).json({});
}

type NextApiRequestWithSvixRequiredHeaders = NextApiRequest & {
headers: IncomingHttpHeaders & WebhookRequiredHeaders;
};

type Event = {
data: Prisma.UserCreateInput;
object: "event";
type: EventType;
};

type EventType = "user.created" | "user.updated" | "*";
James Perkins
James Perkins15mo ago
the code is fine a 404 means the API endpoint you put into Clerk doesn't exist
albatroz
albatroz15mo ago
that's strange it's the deployement url followed by the function /api/webhook/createuser
albatroz
albatroz15mo ago
James Perkins
James Perkins15mo ago
Can you send me the url via DM (assuming you don't wan tto expose it here) so I can look. FYI I work at Clerk in case you are worried about sending random DMs with URLs
albatroz
albatroz15mo ago
Sure
James Perkins
James Perkins15mo ago
Can you send me the middleware file you are using?
albatroz
albatroz15mo ago
i got a error with 400 status
albatroz
albatroz15mo ago
James Perkins
James Perkins15mo ago
that was me 🙂 I was pinging the endpoint directly from my machine.
albatroz
albatroz15mo ago
this is the folder structure.
albatroz
albatroz15mo ago
albatroz
albatroz15mo ago
this is the createUser.ts file
James Perkins
James Perkins15mo ago
Can you send me the middelware file.
albatroz
albatroz15mo ago
And this is the middleware file
import { withClerkMiddleware } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export default withClerkMiddleware((_req: NextRequest) => {
return NextResponse.next();
});

// Stop Middleware running on static files
export const config = {
matcher: [
/*
* Match request paths except for the ones starting with:
* - _next
* - static (static files)
* - favicon.ico (favicon file)
*
* This includes images, and requests from TRPC.
*/
"/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)",
],
};
import { withClerkMiddleware } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export default withClerkMiddleware((_req: NextRequest) => {
return NextResponse.next();
});

// Stop Middleware running on static files
export const config = {
matcher: [
/*
* Match request paths except for the ones starting with:
* - _next
* - static (static files)
* - favicon.ico (favicon file)
*
* This includes images, and requests from TRPC.
*/
"/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)",
],
};
James Perkins
James Perkins15mo ago
Can you do me a favor, can you just change the file to create-user and then redeploy and change in the webhook.
albatroz
albatroz15mo ago
ok no it did not worked
James Perkins
James Perkins15mo ago
So it's something for sure with the deployment... hmm
albatroz
albatroz15mo ago
is there a way to get logs from the vercel function? the console.log like aws cloudWatch
James Perkins
James Perkins15mo ago
If you put logs in it it will log out. but to keep them longer you need to log drain. But if it's 404ing then there are no logs. Running some tests on my side.
albatroz
albatroz15mo ago
Do you need anything from me?
James Perkins
James Perkins15mo ago
Nope I was just deploying the same end point. on my own app to see if it works. and it does. Do you have a repo I can look at?
Entaro
Entaro14mo ago
Just wanted to say thanks for the video/responses! Helped me clarify a couple things!
James Perkins
James Perkins14mo ago
Anytime 🙂