J.Jupid Docs
Embed for partners

Next.js

Next.js route handler example for signing Jupid Embed tokens.

Use a server-only route handler to sign the token. The exact authentication method depends on the partner app; the route must only run for an authenticated partner user.

Install a JWT signer

npm install jose

Route handler

import { SignJWT } from "jose"
import { NextResponse } from "next/server"

const partnerId = process.env.JUPID_EMBED_PARTNER_ID
const secretValue = process.env.JUPID_EMBED_SECRET
if (!partnerId || !secretValue) {
  throw new Error("Missing JUPID_EMBED_PARTNER_ID or JUPID_EMBED_SECRET")
}
const secret = new TextEncoder().encode(secretValue)

export async function GET() {
  const user = await getCurrentUser()
  const now = Math.floor(Date.now() / 1000)
  const token = await new SignJWT({
    email: user.email,
    name: user.name,
    payload: {
      company_name: user.companyName,
    },
  })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(partnerId)
    .setAudience("jupid-embed")
    .setSubject(user.id)
    .setIssuedAt(now)
    .setExpirationTime(now + 300)
    .setJti(crypto.randomUUID())
    .sign(secret)

  return NextResponse.json(
    { token },
    { headers: { "Cache-Control": "no-store" } },
  )
}

getCurrentUser represents the partner app's own authenticated user lookup. Keep this route on the server and never sign tokens from a Client Component.

Client component

Load the SDK with next/script and mount only after it has loaded — useEffect can run before embed.js is available. Return a cleanup that destroys the controller, so React StrictMode re-runs and route changes do not leak iframes or duplicate token fetches.

The partner ID is not a secret; expose it with a NEXT_PUBLIC_ env var.

"use client"

import Script from "next/script"
import { useEffect, useRef, useState } from "react"

export function JupidEmbed() {
  const containerRef = useRef<HTMLDivElement>(null)
  const [sdkReady, setSdkReady] = useState(false)

  useEffect(() => {
    if (!sdkReady) {
      return
    }
    let cancelled = false
    let controller: { destroy(): void } | undefined

    async function mount() {
      if (cancelled || !containerRef.current) {
        return
      }
      controller = window.JupidEmbed.mount({
        partnerId: process.env.NEXT_PUBLIC_JUPID_EMBED_PARTNER_ID ?? "",
        tokenUrl: "/api/jupid/embed-token",
        container: containerRef.current,
        initialPath: "/",
        appUrl: "https://jupid-staging-app.example.com",
      })
    }

    mount()
    return () => {
      cancelled = true
      controller?.destroy()
    }
  }, [sdkReady])

  return (
    <>
      <Script
        onLoad={() => setSdkReady(true)}
        src="https://jupid-staging-app.example.com/embed.js"
        strategy="afterInteractive"
      />
      <div className="min-h-[720px]" ref={containerRef} />
    </>
  )
}

If your TypeScript setup needs the browser global, declare it in the partner app:

declare global {
  interface Window {
    JupidEmbed: {
      mount(options: {
        partnerId: string
        token?: string
        tokenUrl?: string
        container: HTMLElement
        initialPath?: string
        appUrl?: string
      }): {
        destroy(): void
      }
    }
  }
}

On this page