Next.js App Router — Full-Stack Integration
Both halves of Consenti living inside one Next.js app — no separate Node.js server to deploy or scale. The backend is mounted on a catch-all Route Handler; the widget is a Client Component in the root layout, dynamically imported so the browser-only code never runs during SSR.
What this demonstrates
- Backend mounted on
app/consenti/[...path]/route.ts— one deploy target - Widget as a Client Component, dynamically imported to stay out of the SSR bundle
- Environment-driven config so the same code works in dev and production
1. Mount the backend
app/consenti/[...path]/route.ts
ts
import { createConsenti } from '@consenti/api'
import type { NextRequest } from 'next/server'
let consenti: Awaited<ReturnType<typeof createConsenti>>
async function getConsenti() {
if (!consenti) {
consenti = createConsenti({
storage: { driver: 'node:sqlite', path: './consenti-data' },
auth: {
mode: 'local',
adminEmail: process.env.CONSENTI_ADMIN_EMAIL!,
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
},
dashboard: true,
})
}
return consenti
}
async function consentiHandler(req: NextRequest) {
const c = await getConsenti()
return c.handleRequest(req)
}
export { consentiHandler as GET, consentiHandler as POST, consentiHandler as PUT, consentiHandler as DELETE, consentiHandler as PATCH }Admin dashboard now lives at /consenti/ on the same origin as your app — no CORS configuration needed.
2. Mount the widget
components/ConsentSetup.tsx
tsx
'use client'
import { useEffect, useRef } from 'react'
import type { ConsentiSetup as WidgetType } from '@consenti/ui'
export function ConsentSetup() {
const widgetRef = useRef<WidgetType | null>(null)
useEffect(() => {
let widget: WidgetType
import('@consenti/ui').then(({ ConsentiSetup }) => {
widget = new ConsentiSetup({
api: { enabled: true, baseUrl: process.env.NEXT_PUBLIC_API_URL },
core: { autoHonorGPC: true },
})
widgetRef.current = widget
})
return () => widgetRef.current?.destroy()
}, [])
return null
}app/layout.tsx
tsx
import { ConsentSetup } from '@/components/ConsentSetup'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ConsentSetup />
{children}
</body>
</html>
)
}💡Because the backend and frontend share an origin, set
NEXT_PUBLIC_API_URLto your own site's URL (or leave it unset — the widget defaults to same-origin when baseUrl is omitted and api.enabled: true).3. Read consent in a Server Component
Server Components can't read the widget's browser cookie directly for banner state, but they can query the backend's REST API for reporting or gating server-side logic:
ts
// app/admin/consent-stats/page.tsx
async function getStats() {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/consenti/api/v1/consent/${visitorId}`, {
headers: { Authorization: `Bearer ${adminToken}` },
cache: 'no-store',
})
return res.json()
}Related documentation