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
createUnchurnHandlerimported from@unchurn.dev/widget/nextjs- One of the auth systems below already configured in your project
- A way to look up the user’s
stripeSubscriptionIdfrom 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 Request — req.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
- Next.js App Router — route handler placement and multi-environment setup
- Auth tokens reference — token format, TTL options, and rotation
- Widget API reference — full
createUnchurnHandleroptions