Next.js routes
The server half: a catch-all route handler that proxies component requests to the Handset API. Your key stays server-side; your session decides tenant access.
Installation
npx shadcn@latest add @handset/next-routesLands at app/api/handset/[...handset]/route.ts and expects HANDSET_API_KEY in your environment.
The auth boundary
One function is yours to implement. Everything the components can see flows through it:
async function resolveTenantId(req: NextRequest): Promise<string | null> {
const session = await auth(); // your auth library
if (!session) throw new Response(null, { status: 401 });
return session.organization.handsetTenantId; // "tnt_…"
}The resolved tenant is injected into every read — anything the browser claims is ignored. Return null for single-tenant accounts.
The allowlist
Only the endpoints messaging components need are forwarded; everything else 404s. Buying numbers, porting, compliance — none of that is reachable from the browser. Extend the list deliberately if you build more:
const ALLOWED = [
{ method: "GET", match: (p) => p === "conversations" },
{ method: "GET", match: (p) => /^conversations\/[\w]+$/.test(p) },
{ method: "GET", match: (p) => p === "messages" },
{ method: "POST", match: (p) => p === "messages" },
{ method: "GET", match: (p) => p === "voicemails" },
{ method: "GET", match: (p) => /^voicemails\/[\w]+$/.test(p) },
];Environment
HANDSET_API_KEY=sk_test_… # required; sk_live_… in production
HANDSET_API_URL= # optional override, defaults to https://api.handset.dev/v1Not on Next.js? The file is ~100 lines of fetch with no framework magic — port it to Express, Hono, or Rails in a few minutes. The components only care that baseUrl speaks the same paths.