Skip to content

@nexafon/react works in the app router: every export is marked 'use client' and nothing touches window during server rendering.

Terminal window
npm install @nexafon/react @nexafon/node

The secret key stays on the server, in a route handler (NEXAFON_SECRET_KEY in the server’s environment, never a NEXT_PUBLIC_ variable):

examples/next/app/api/nexafon-token/route.ts
import { createUserToken } from '@nexafon/node'
interface SignedInUser {
id: string
name: string
role: string
email: string
emailVerified: boolean
isStaff: boolean
}
// Your app's own session decides who is signed in, and its own records whether it has confirmed their email. The
// example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session.
function signedInUser(_req: Request): SignedInUser | null {
const email = process.env.EXAMPLE_DEMO_USER_EMAIL
if (!email) return null
return {
id: 'demo-user-1',
name: 'Demo User',
role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent',
email,
// The demo address is a member you invited, so you have confirmed it.
emailVerified: true,
isStaff: true,
}
}
// Everyone who gets a token may reach your workspace's inbox and calls: sign only for your staff, never for customers
// who signed up. The example's only user is the demo teammate.
function mayUseNexafon(user: SignedInUser): boolean {
return user.isStaff
}
// Signs a token for the signed-in user with NEXAFON_SECRET_KEY.
export function POST(req: Request) {
const user = signedInUser(req)
if (!user) return new Response(null, { status: 401 })
if (!mayUseNexafon(user)) return new Response(null, { status: 403 })
const token = createUserToken({
userId: user.id,
name: user.name,
role: user.role,
email: user.email,
// Nexafon links a first sign-in to the member with this email only when it is verified: never send true for an
// address your app has not confirmed, or whoever sets a member's email on your app acts as that member.
emailVerified: user.emailVerified,
})
return new Response(token, {
headers: { 'content-type': 'text/plain', 'cache-control': 'no-store' },
})
}

signedInUser stands for your app’s own session: answer 401 when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in EXAMPLE_DEMO_USER_EMAIL and answers 401 without one; replace it with your session. mayUseNexafon answers 403 to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff.

getUserToken is a function, so render <NexafonProvider> from a client component ('use client'):

examples/next/app/support.tsx
'use client'
import { CallButton, Inbox, NexafonProvider, Softphone } from '@nexafon/react'
// getUserToken is a function, so the provider is rendered from a client component.
const getUserToken = async () => {
const r = await fetch('/api/nexafon-token', { method: 'POST' })
if (!r.ok) throw new Error(`The token endpoint answered ${r.status}`)
return r.text()
}
export function Support({ publishableKey }: { publishableKey: string }) {
return (
<NexafonProvider publishableKey={publishableKey} getUserToken={getUserToken}>
<main style={{ display: 'grid', gap: 16, padding: 16, gridTemplateColumns: '1fr 340px' }}>
<Inbox style={{ height: 640 }} />
<aside>
<CallButton phone="+252615550142" />
<Softphone />
</aside>
</main>
</NexafonProvider>
)
}

The page is a server component. It reads NEXAFON_PUBLISHABLE_KEY on each request (dynamic = 'force-dynamic'), so one build serves any key, and passes it to the client component:

examples/next/app/page.tsx
import { Support } from './support'
// Read on each request, so one build serves any key.
export const dynamic = 'force-dynamic'
export default function Page() {
return <Support publishableKey={process.env.NEXAFON_PUBLISHABLE_KEY ?? ''} />
}

Use the React components and hooks inside it as usual. The full example is examples/next.