Skip to Content
WidgetAuth integrations

Auth integrations

This page shows you how to wire resolveUser inside createUnchurnHandler for four common auth systems: Clerk, NextAuth / Auth.js, Supabase Auth, and a custom header-based approach.

resolveUser is your auth boundary. Return null to reject (handler responds 401). Return { subscriptionId, mode? } to mint a signed token. The handler never reads your auth state directly — you own that logic entirely.

Prerequisites

  • createUnchurnHandler imported from @unchurn.dev/widget/nextjs
  • One of the auth systems below already configured in your project
  • A way to look up the user’s stripeSubscriptionId from your database

Clerk

// app/api/unchurn/token/route.ts import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs' import { auth } from '@clerk/nextjs/server' import { db } from '@/lib/db' export const POST = createUnchurnHandler({ secret: process.env.UNCHURN_SECRET!, merchantId: process.env.UNCHURN_MERCHANT_ID!, resolveUser: async () => { const { userId } = await auth() if (!userId) return null const user = await db.user.findUnique({ where: { clerkId: userId }, select: { stripeSubscriptionId: true }, }) if (!user?.stripeSubscriptionId) return null return { subscriptionId: user.stripeSubscriptionId } }, })

auth() from @clerk/nextjs/server reads the request context automatically — you do not need to pass req. Return null if userId is absent (unauthenticated) or if the user has no subscription (handler responds 401 in both cases).


NextAuth / Auth.js

// app/api/unchurn/token/route.ts import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs' import { auth } from '@/auth' import { db } from '@/lib/db' export const POST = createUnchurnHandler({ secret: process.env.UNCHURN_SECRET!, merchantId: process.env.UNCHURN_MERCHANT_ID!, resolveUser: async (req) => { // `req` carries the cookie header; `auth()` reads it here. const session = await auth() if (!session?.user?.id) return null const user = await db.user.findUnique({ where: { id: session.user.id }, select: { stripeSubscriptionId: true }, }) if (!user?.stripeSubscriptionId) return null return { subscriptionId: user.stripeSubscriptionId } }, })

Auth.js v5 auth() resolves the session from the cookie automatically when called inside a route handler. Earlier Auth.js / NextAuth v4 versions use getServerSession(authOptions) from next-auth/next instead.


Supabase Auth

// app/api/unchurn/token/route.ts import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs' import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' import { cookies } from 'next/headers' import { db } from '@/lib/db' export const POST = createUnchurnHandler({ secret: process.env.UNCHURN_SECRET!, merchantId: process.env.UNCHURN_MERCHANT_ID!, resolveUser: async () => { const supabase = createRouteHandlerClient({ cookies }) const { data: { user } } = await supabase.auth.getUser() if (!user) return null const profile = await db.profile.findUnique({ where: { supabaseId: user.id }, select: { stripeSubscriptionId: true }, }) if (!profile?.stripeSubscriptionId) return null return { subscriptionId: profile.stripeSubscriptionId } }, })

createRouteHandlerClient reads the Supabase session from the Next.js cookie store. getUser() makes a network round-trip to verify the JWT — prefer it over getSession() for server-side auth checks.


Custom header-based auth

For internal tools, mobile apps, or service-to-service calls that pass a bearer token in a header:

// app/api/unchurn/token/route.ts import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs' import { verifyInternalToken } from '@/lib/tokens' import { db } from '@/lib/db' export const POST = createUnchurnHandler({ secret: process.env.UNCHURN_SECRET!, merchantId: process.env.UNCHURN_MERCHANT_ID!, resolveUser: async (req) => { const header = req.headers.get('authorization') ?? '' const bearer = header.startsWith('Bearer ') ? header.slice(7) : null if (!bearer) return null const claims = await verifyInternalToken(bearer) if (!claims?.userId) return null const user = await db.user.findUnique({ where: { id: claims.userId }, select: { stripeSubscriptionId: true }, }) if (!user?.stripeSubscriptionId) return null return { subscriptionId: user.stripeSubscriptionId } }, })

req is a standard Requestreq.headers.get(name) works in App Router route handlers and in any Node 18+ Request-compatible context. You can also read cookies via req.headers.get('cookie') if your auth system uses cookies directly.


Common pitfalls

Returning a subscription ID that belongs to a different user. resolveUser is the trust boundary. If a bug causes you to return the wrong subscription ID, the widget will mint a valid token for that subscription. Always derive the subscription ID from the authenticated user’s identity, not from a request body parameter.

Not handling the case where the user has no subscription. Return null when stripeSubscriptionId is absent. The handler responds 401, which useUnchurn surfaces as hook.error. Show the user a message rather than letting the hook silently stay in isReady: false.

Throwing inside resolveUser. If your database call throws, the handler returns 500. The error message is logged server-side only — nothing leaks to the browser. Add your own error handling inside resolveUser if you need granular fallback behavior.


Next steps

Last updated on