Skip to Content
WidgetNext.js App Router

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/widget installed (pnpm add @unchurn.dev/widget)
  • UNCHURN_SECRET and UNCHURN_MERCHANT_ID in 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 integrationsresolveUser examples 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
Last updated on