@idhub/identity-adapter-nextjs
v0.1.0
Published
Next.js App Router adapter for @idhub/identity-core: Edge-safe middleware that guarantees the canonical anonymous id cookie, plus an <IdentityProvider> client component and useIdentity() hook.
Readme
@idhub/identity-adapter-nextjs
Next.js App Router adapter for @idhub/identity-core. Thin
by design: it owns no vendor knowledge and no cookie logic — every id is created
and persisted by identity-core.
Three entry points, deliberately separate so a Server Component or Edge
middleware never pulls React (or a "use client" boundary) into its bundle:
| Import | Runtime | Purpose |
| --- | --- | --- |
| @idhub/identity-adapter-nextjs/middleware | Edge | Guarantee the anonymous-id cookie on every request |
| @idhub/identity-adapter-nextjs/server | Server Components / Route Handlers | Read the anonymous id |
| @idhub/identity-adapter-nextjs | Client Components | <IdentityProvider> + useIdentity() |
1. Middleware
Apps with no middleware of their own:
// middleware.ts
import { identityMiddleware } from '@idhub/identity-adapter-nextjs/middleware'
export const middleware = identityMiddleware()
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }Apps that already have middleware logic compose with withIdentity, which
returns the pieces instead of wrapping your handler — so you keep control flow
(redirects, rewrites, 401s) and still get the cookie guarantee on every path:
// middleware.ts
import { withIdentity, applyAnonymousIdCookie } from '@idhub/identity-adapter-nextjs/middleware'
import { NextResponse, type NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { response, anonymousId, setCookieHeader } = withIdentity(request)
if (!isAuthorized(request)) {
// Your own response still gets the cookie.
return applyAnonymousIdCookie(NextResponse.redirect(new URL('/login', request.url)), setCookieHeader)
}
response.headers.set('x-anon-id', anonymousId)
return response
}withIdentity also writes the id back onto the incoming request when it had
to generate one. That is what makes the id visible to Server Components on the
first request, rather than only from the second page load onwards.
Everything in this entry point's import graph is Edge-safe: only
Request/Response/Headers and crypto.randomUUID, via identity-core.
2. Provider (App Router layout.tsx)
// app/layout.tsx — a Server Component
import { getAnonymousIdFromCookies } from '@idhub/identity-adapter-nextjs/server'
import { cookies } from 'next/headers'
import { Providers } from './providers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const anonymousId = getAnonymousIdFromCookies(await cookies())
return (
<html lang="en">
<body>
<Providers initialAnonymousId={anonymousId}>{children}</Providers>
</body>
</html>
)
}// app/providers.tsx
'use client'
import { IdentityProvider } from '@idhub/identity-adapter-nextjs'
export function Providers({ initialAnonymousId, children }) {
return (
<IdentityProvider providers={[segment(), launchDarkly()]} initialAnonymousId={initialAnonymousId}>
{children}
</IdentityProvider>
)
}Why initialAnonymousId is an explicit prop from the server
<IdentityProvider> is a Client Component, but Next.js still server-renders
it. On the server there is no document, so deriving the initial state from
document.cookie would fall back to a freshly generated id that disagrees with
the one the browser actually holds — a hydration mismatch on every cold load.
Passing the server-read value down makes the SSR pass and the hydration pass
render the same id. After hydration the IdentityClient (which reads the
cookie) becomes the source of truth.
The IdentityClient itself is constructed once, in an effect, browser-side
only — never during render, because constructing it fans onAnonymous out to
every vendor SDK.
3. useIdentity()
'use client'
import { useIdentity } from '@idhub/identity-adapter-nextjs'
export function LoginButton() {
const { state, identify, reset, track, page } = useIdentity()
return <button onClick={() => identify('user-123', { plan: 'pro' })}>{state.status}</button>
}Throws a descriptive error outside <IdentityProvider> rather than returning a
silent no-op, because a missing provider means every vendor call is being
dropped on the floor.
The vendor-facing
IdentityProviderinterface is not re-exported here — this package already exports a React component by that name. Import it from@idhub/identity-core.
Testing
test-app/ is a throwaway App Router app (one page, a login button, a logout
button) driven by the Playwright suite in e2e/. It uses a test-double provider
that records every fan-out call on window.__identityCalls, so the specs can
assert what the vendors would have been handed.
pnpm --filter @idhub/identity-adapter-nextjs test:e2eCovers: cold load with no cookie → Set-Cookie on the response; login →
onIdentify with the right userId and the canonical anonymous id; logout → a
new anonymous id that is never the pre-logout one.
