Nov 09, 2023
Jacob Evans
Utilize Clerk Metadata & Stripe Webhooks for efficient user data management and enhanced SaaS experiences with our step-by-step tutorial.
By putting Clerk’s user metadata types to work, developers can proficiently handle user data, making their SaaS integrations run smoother, and work harder. It's like adding a turbocharger to your product's engine, enhancing functionality and improving the user experience, for a more comprehensive, customizable, and synchronizing systems ready to build any SaaS product out there.
A great feature in the wild world of SaaS product development to power integrations powering integrations with other powerful products. We're talking about a sturdy, malleable means for handling user data.
You've got three types of User Metadata – public, private, and unsafe. Each one has its own unique access level and use case.
Harnessing the power of User Metadata in tandem with Stripe’s webhooks offers significant advantages in SaaS product development. Clerk Metadata's flexible user data management paired with Stripe webhooks' real-time transaction updates creates a robust, efficient system. This combination ensures both comprehensive user data handling and prompt responsiveness to transaction events. Utilizing Clerk Metadata alongside Stripe’s webhooks lends itself well for streamlined and user-friendly SaaS development.
Utilizing Clerk's public User Metadata offers significant advantages for managing user data and transactions in your SaaS product. It allows for real-time updates, such as including a "paid" field after a transaction, offering a clear snapshot of payment statuses. This use of public metadata improves transparency, boosts data management efficiency, and enhances the overall user experience.
The first step will be setting up accounts at Clerk & Stripe. Once you have those accounts you will follow the well documented Clerk’s Next.js Quickstart Guide. To have access to the correct data from the Clerk session you will need to access the custom session data on the Dashboard, we will edit the session data to look like this:
1{2"publicMetadata": "{{user.public_metadata}}"3}
The last part will be setting up from the Stripe quickstart, the basic Stripe webhook. We will modify it later for our own needs, and there will also be a repo for you to grab afterwards! By the end of the quickstarts, you should have something in your .env.local that looks like this.
Environment Variables1# CLERK2NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***3CLERK_SECRET_KEY=sk_test_***45# STRIPE6NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_***7STRIPE_SECRET_KEY=sk_test_***8STRIPE_WEBHOOK_SECRET=whsec_***
Once we have everything installed we are going to jump into a very basic app, with one private route /members, and the homepage route will serve as our public route where all the fun stuff will happen. Our Middleware is going to be handling the access.
Middleware Routes1export default authMiddleware({2// Making sure homepage route and API, especially the webhook, are both public!3publicRoutes: ["/", "/api/(.*)"],4afterAuth: async (auth, req) => {5// Nice try, you need to sign-in6if (!auth.userId && !auth.isPublicRoute) {7return redirectToSignIn({ returnBackUrl: req.url });8}9// Hey! Members is for members 😆10if (11auth.userId &&12req.nextUrl.pathname === "/members" &&13auth.sessionClaims.publicMetadata?.stripe?.payment !== "paid"14) {15return NextResponse.redirect(new URL("/", req.url));16}17// Welcome paid member! 👋18if (19auth.userId &&20req.nextUrl.pathname === "/members" &&21// How we get payment value "paid" is next, in the webhook section!22auth.sessionClaims.publicMetadata?.stripe?.payment === "paid"23) {24return NextResponse.next();25}26// If we add more public routes, signed-in people can access them27if (auth.userId && req.nextUrl.pathname !== "/members") {28return NextResponse.next();29}30// Fallthrough last-ditch to allow access to a public route explicitly31if (auth.isPublicRoute) {32return NextResponse.next();33}34},35});
We can simplify the Middleware access logic for this app, but this explicit example can show how you can have far more complex access handling. Where do we get paid from!? That is coming up next.
Since this app is our SaaS with member access, we need to provide a way for the user to pay and gain access. Let’s start with setting up the tokens for Clerk & instantiating Stripe.
1const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {2apiVersion: "2023-10-16",3});4const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string;
After we have the credentials, the rest of the code should look very similar to the Stripe default logic for webhooks.
1export async function POST(req: NextRequest) {2if (req === null)3throw new Error(`Missing userId or request`, { cause: { req } });4// Stripe sends this for us 🎉5const stripeSignature = req.headers.get("stripe-signature");6// If we don't get it, we can't do anything else!7if (stripeSignature === null) throw new Error("stripeSignature is null");89let event;10try {11event = stripe.webhooks.constructEvent(12await req.text(),13stripeSignature,14webhookSecret15);16} catch (error) {17if (error instanceof Error)18return NextResponse.json(19{20error: error.message,21},22{23status: 400,24}25);26}27// If we dont have the event, we can't do anything again28if (event === undefined) throw new Error(`event is undefined`);29switch (event.type) {30case "checkout.session.completed":31const session = event.data.object;32console.log(`Payment successful for session ID: ${session.id}`);33break;34default:35console.warn(`Unhandled event type: ${event.type}`);36}3738return NextResponse.json({ status: 200, message: "success" });39}
So what is next!? We need a way to know when a Clerk User has "paid." Well, let's extract that switch statement and add the secret sauce. That'll make this all work when we are done!
Adding Clerk User Metadata to Event1switch (event.type) {2case "checkout.session.completed":3const session = event.data.object;4console.log(`Payment successful for session ID: ${session.id}`);5// That's it? Yep, that's it. We love UX 🎉6clerkClient.users.updateUserMetadata(7event.data.object.metadata?.userId as string,8{9publicMetadata: {10stripe: {11status: session.status,12// This is where we get "paid"13payment: session.payment_status,14},15},16}17);18break;19default:20console.warn(`Unhandled event type: ${event.type}`);21}
Some of you may have noticed event.data.object.metadata?.userId (where did that come from!?). We will get to that one too. The reason for this is that we can’t access Clerk’s session in the webhook, so we will get a little creative.
We will now need to create an endpoint that will generate our Stripe session that will be used to make our payment and turn our Clerk User into a paid Member. This is where the userId in the webhook will also be coming from! Instantiate stripe the same as before, it will again be a Next.js POST endpoint.
Stripe Session1// This is our Clerk function for session User2const { userId } = auth();3// We are receiving this from the Client request, thats next!4const { unit_amount, quantity } = await req.json();56try {7const session = await stripe.checkout.sessions.create({8payment_method_types: ["card"],9line_items: [10{11price_data: {12currency: "usd",13product_data: {14name: "Membership",15},16unit_amount,17},18quantity,19},20],21// This is where "event.data.object.metadata?.userId" is defined!22metadata: {23userId,24},25mode: "payment",26success_url: `${req.headers.get("origin")}/members`,27cancel_url: `${req.headers.get("origin")}/`,28});2930return NextResponse.json({ session }, { status: 200 });31} catch (error) {32...33}
We have now laid the groundwork for a SaaS leveraging Clerk’s User Metadata to manage User specific data! So, to really focus on the versatility and potential of this feature, the UI portion has been kept really simple. We have the Homepage with a button to navigate to /members page and to become a paid member, let’s take a look at the homepage.
Homepage Implementation1export default function Home() {2const { isSignedIn } = useAuth();34return (5<main>6{!isSignedIn ? (7<div className="...">8<SignIn redirectUrl="/" />9</div>10) : (11<div>12<div className="...">You are signed in!</div>13<div className="...">14<CheckoutButton />15<a16className="..."17href="/members"18>19Go to members page20</a>21</div>22</div>23)}24</main>25);26}
This pattern can be used with any other transactions or user specific data you would like to handle in the backend and then utilize in the client. This keeps your User Management pragmatic & versatile, offloading the burden across multiple systems. This is only the beginning with what we can do with Clerk’s toolset, this time we only leveraged User Metadata! What should we do next? Let us know in the Discord
Not forgetting, you will want the complete codebase to check out, and learn from!
Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.