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-routes

Lands 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/v1

Not 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.