Consenti

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()
}