Next.js App Router
This page shows you how to integrate the Unchurn widget into a Next.js 13.4+ App Router project, covering route handler placement, 'use client' boundaries, multi-environment env vars, and middleware compatibility.
The Quickstart covers the basic three-step path. Come here when you need more than that.
Prerequisites
- Next.js 13.4 or later (App Router)
@unchurn.dev/widgetinstalled (pnpm add @unchurn.dev/widget)UNCHURN_SECRETandUNCHURN_MERCHANT_IDin your environment (see below)
1. Place the route handler
Create the token-signing endpoint inside app/api/. The app/api/unchurn/token/ path is a convention, not a requirement — put it wherever your project keeps API routes.
// app/api/unchurn/token/route.ts
import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs'
import { getCurrentUser } from '@/lib/auth'
export const POST = createUnchurnHandler({
secret: process.env.UNCHURN_SECRET!,
merchantId: process.env.UNCHURN_MERCHANT_ID!,
resolveUser: async (req) => {
const user = await getCurrentUser(req)
if (!user?.stripeSubscriptionId) return null
return { subscriptionId: user.stripeSubscriptionId }
},
})resolveUser is the only code you write. Return null for unauthenticated requests — the handler responds 401. Return an object with subscriptionId and the handler mints a signed token. See Auth integrations for Clerk, NextAuth, and Supabase examples.
Verify: curl -X POST http://localhost:3000/api/unchurn/token should return 401 (unauthenticated). Authenticated via your session cookie, it should return { authToken, expiresAt, merchantId, subscriptionId, mode }.
2. Mark the component boundary
The useUnchurn hook runs in the browser. Add 'use client' to any component that calls it. Server Components higher in the tree are unaffected — the hook never touches the server.
// app/components/cancel-button.tsx
'use client'
import { useUnchurn } from '@unchurn.dev/widget/react'
export function CancelButton() {
const { show, isReady, error } = useUnchurn({
tokenEndpoint: '/api/unchurn/token',
})
if (error) {
return <p>Could not load cancel flow. Reload the page to try again.</p>
}
return (
<button disabled={!isReady} onClick={() => void show()}>
Cancel subscription
</button>
)
}Import CancelButton from any Server Component — including layouts, pages, and async Server Components — without adding 'use client' to those files.
Verify: The button renders disabled briefly on first mount, then becomes active once the token prefetch resolves.
3. Set environment variables per environment
Next.js exposes env vars differently across Vercel environments. Use .env.local for local dev and Vercel’s project settings for preview and production.
# .env.local — local dev only, never commit
UNCHURN_SECRET=… # 32-byte hex from your dashboard
UNCHURN_MERCHANT_ID=mch_…On Vercel, set both variables in Project Settings → Environment Variables with separate values per environment scope (Development, Preview, Production). For test-mode preview deployments, pass mode: 'test' from resolveUser:
// app/api/unchurn/token/route.ts
import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs'
import { getCurrentUser } from '@/lib/auth'
export const POST = createUnchurnHandler({
secret: process.env.UNCHURN_SECRET!,
merchantId: process.env.UNCHURN_MERCHANT_ID!,
resolveUser: async (req) => {
const user = await getCurrentUser(req)
if (!user?.stripeSubscriptionId) return null
return {
subscriptionId: user.stripeSubscriptionId,
mode: process.env.NODE_ENV === 'production' ? 'live' : 'test',
}
},
})Verify: unch_test_ prefix in the token response on preview; unch_live_ on production.
4. Middleware compatibility
If you use next/server middleware to protect routes, the /api/unchurn/token endpoint must be reachable by authenticated users. Add it to your public-path matcher or ensure the middleware forwards authenticated requests through.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Allow the unchurn token endpoint through your auth middleware.
// The handler itself calls resolveUser — auth is enforced there.
if (request.nextUrl.pathname === '/api/unchurn/token') {
return NextResponse.next()
}
// ...your existing middleware logic
}
export const config = {
matcher: ['/dashboard/:path*'],
}Verify: A signed-in user on /dashboard can open the widget. An unauthenticated user gets 401 from the handler — not a redirect to /login from middleware.
Common pitfalls
UNCHURN_SECRET exposed to the browser. Any variable without NEXT_PUBLIC_ stays server-side in Next.js. Never prefix these. If you see the secret in client bundle output, remove the prefix immediately.
'use client' creep up the tree. Adding 'use client' to a layout or page file makes every child a Client Component. Keep the hook in a leaf component; pass only callbacks or data down from Server Components via props.
Route handler not exported as POST. App Router enforces named HTTP-method exports. export default or export const handler will not match POST requests. The handler returns 405 for non-POST methods regardless, but the export name must be POST.
Next steps
- Auth integrations —
resolveUserexamples for Clerk, NextAuth, Supabase, and custom headers - Test & live modes — switching Stripe environments and what the TEST badge means
- Configuration — offer options, copy overrides, and theming