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 joseRoute 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
}
}
}
}