Browse documentation
Quickstart · Node.js 20+

Build a complete Node.js launch route

This example uses a Next.js route handler, but the signing function works in Express, Fastify, NestJS, or any Node server. The route assumes your application has already authenticated the user.

1. Configure environment variables

NANAADE_API_ORIGIN=https://api.nanaade.ai
NANAADE_TENANT_ORIGIN=https://northbridge.nanaade.ai
NANAADE_RETURN_PATH=/dashboard
NANAADE_SSO_CLIENT_ID=tn_replace_with_client_id
NANAADE_SSO_SECRET=replace_with_reveal_once_secret

Do not prefix the secret with NEXT_PUBLIC_. Validate these variables during application startup so a missing secret fails deployment rather than a user's launch.

2. Create the signing function

typescript
import crypto from "node:crypto";

type NanaadeIdentity = {
id: string;       // permanent ID in your system
email: string;
};

export function createNanaadeAssertion(user: NanaadeIdentity) {
const secret = process.env.NANAADE_SSO_SECRET;
const clientId = process.env.NANAADE_SSO_CLIENT_ID;
const redirectOrigin = process.env.NANAADE_TENANT_ORIGIN;
if (!secret || !clientId || !redirectOrigin) {
  throw new Error("Nanaade SSO configuration is incomplete");
}

const now = Math.floor(Date.now() / 1000);
const assertion = {
  sub: user.id,
  email: user.email.trim().toLowerCase(),
  iat: now,
  exp: now + 300,
  nonce: crypto.randomBytes(24).toString("base64url"),
  redirectOrigin: redirectOrigin.replace(/\/$/, ""),
  returnPath: process.env.NANAADE_RETURN_PATH || "/dashboard",
};

const payload = Buffer
  .from(JSON.stringify(assertion), "utf8")
  .toString("base64url");

const signature = crypto
  .createHmac("sha256", secret)
  .update(payload)
  .digest("base64url");

return { clientId, payload, signature };
}

Generate this object per click. Never cache it: the nonce is single-use and the time window is short.

3. Escape form values

Because values are inserted into HTML, escape them even though Nanaade-generated identifiers are constrained:

typescript
function escapeHtml(value: string) {
return value.replace(/[&<>"']/g, (character) => ({
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&quot;",
  "'": "&#39;",
})[character]!);
}

4. Add the launch route

import { NextResponse } from "next/server";
import { createNanaadeAssertion } from "@/lib/nanaade-sso";
import { getAuthenticatedUser } from "@/lib/auth";

export async function GET() {
  const user = await getAuthenticatedUser();
  if (!user) {
    return NextResponse.redirect(new URL("/login", process.env.PORTAL_ORIGIN));
  }

  const fields = createNanaadeAssertion({ id: user.id, email: user.email });
  const endpoint = new URL(
    "/api/v1/sso/exchange",
    process.env.NANAADE_API_ORIGIN,
  ).toString();

  const inputs = Object.entries(fields).map(([name, value]) =>
    `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`
  ).join("");

  const html = `<!doctype html>
  <html lang="en"><head><meta charset="utf-8"><title>Opening Nanaade</title></head>
  <body>
    <p>Opening your Nanaade workspace…</p>
    <form id="nanaade" method="post" action="${escapeHtml(endpoint)}">
      ${inputs}
      <noscript><button type="submit">Continue to Nanaade</button></noscript>
    </form>
    <script>document.getElementById("nanaade").submit()</script>
  </body></html>`;

  return new NextResponse(html, {
    headers: {
      "content-type": "text/html; charset=utf-8",
      "cache-control": "no-store",
      "content-security-policy":
        `default-src 'none'; script-src 'unsafe-inline'; form-action ${new URL(endpoint).origin}`,
      "referrer-policy": "no-referrer",
    },
  });
}

Link to the route from your authenticated UI:

<a href="/api/nanaade/launch">Open Nanaade career workspace</a>

Express equivalent

app.get("/integrations/nanaade/launch", requireUser, (req, res) => {
  const fields = createNanaadeAssertion({
    id: req.user.id,
    email: req.user.email,
  });
  // Render the same escaped, no-store auto-submit form shown above.
  res.status(200).type("html").set("Cache-Control", "no-store").send(renderForm(fields));
});

Common implementation mistakes

| Mistake | Result | |---|---| | Signing JSON.stringify(assertion) instead of encoded payload | SSO_SIGNATURE_INVALID | | Using Date.now() directly | Timestamp is milliseconds; assertion rejected | | Reusing a generated form | SSO_ASSERTION_REPLAYED or expiry | | Using email as sub | Identity breaks after email changes | | Calling exchange with backend fetch() | User browser does not receive the intended Nanaade session | | Putting secret in client code | Credential compromise; rotate immediately |

The repository's tenant_system application is the runnable reference implementation for valid, expired, replayed, invalid-signature, inactive-member, and unknown-member scenarios.