Examples
Browser example
A raw JavaScript integration that verifies the parent, authenticates, and starts your app.
Where this runs
Run this code inside your hosted app iframe. Your app must first allow Zetapad in its frame-ancestors header. Supply startYourApp and showLoginError, and connect /zetapad/state and /zetapad/login to the server example.
Keep protected UI hidden while loading. startYourApp runs only after a valid parent handshake and successful backend login. If showLoginError is called, display the error and leave the app blocked.
The version 2 example rejects top-level startup, checks both fixed parent origins and the source window, pins the responding origin, validates code-only replies, and binds login to a backend-generated state challenge. Identity and approved display fields come from your backend after code exchange and userinfo. Use the backend-verified member.userId for persistent experiences across apps, including when no scopes are registered. It is raw JavaScript, not an SDK.
Complete browser example
// Run inside the app iframe. Trust only Zetapad's two production origins.
const ZETAPAD_ORIGINS = ["https://zetapad.com", "https://www.zetapad.com"];
let parentOrigin = null;
const pending = new Map();
window.addEventListener("message", (event) => {
const reply = event.data;
if (event.source !== window.parent || !ZETAPAD_ORIGINS.includes(event.origin)
|| (parentOrigin && event.origin !== parentOrigin)
|| !reply || typeof reply !== "object" || Array.isArray(reply)
|| reply.channel !== "zetapad" || reply.version !== 2
|| typeof reply.requestId !== "string"
|| ("result" in reply) === ("error" in reply)) return;
const request = pending.get(reply.requestId);
if (!request) return;
if (reply.error && (typeof reply.error.code !== "string"
|| typeof reply.error.message !== "string")) return;
if ("error" in reply && !reply.error) return;
if (request.method === "ready" && !reply.error) {
if (typeof reply.result?.launchId !== "string" || !reply.result.launchId
|| typeof reply.result.appId !== "string" || !reply.result.appId) return;
parentOrigin = event.origin; // Pin all further messages to this exact parent.
}
if (request.method === "authenticate" && !reply.error
&& (typeof reply.result?.code !== "string" || !reply.result.code
|| reply.result.code.length > 128 || reply.result.expiresIn !== 60
|| typeof reply.result.state !== "string" || !reply.result.state
|| reply.result.state.length > 256
|| Object.keys(reply.result).some((key) => !["code", "expiresIn", "state"].includes(key)))) return;
pending.delete(reply.requestId);
clearTimeout(request.timeout);
if (reply.error) request.reject(Object.assign(
new Error(reply.error.message), { code: reply.error.code }));
else request.resolve(reply.result);
});
function callZetapad(method, params) {
if (method !== "ready" && !parentOrigin) {
return Promise.reject(new Error("Complete the ready handshake first"));
}
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
// Allow the member time to read and approve the consent dialog.
const timeout = setTimeout(() => {
pending.delete(requestId);
reject(new Error("Zetapad did not respond. Retry or reopen the room."));
}, 120000);
pending.set(requestId, { method, resolve, reject, timeout });
// Only the matching origin receives ready; later calls use the pinned origin.
for (const origin of parentOrigin ? [parentOrigin] : ZETAPAD_ORIGINS) {
window.parent.postMessage({ channel: "zetapad", version: 2,
requestId, method, params }, origin);
}
});
}
async function enterRoom() {
if (window.self === window.top) {
throw new Error("Open this app from Zetapad Rooms.");
}
const { launchId } = await callZetapad("ready", {});
// The app backend stores state in the visitor's server-side session.
const stateResponse = await fetch("/zetapad/state", {
credentials: "same-origin", cache: "no-store"
});
if (!stateResponse.ok) throw new Error("App login challenge failed");
const challenge = await stateResponse.json();
const authorization = await callZetapad("authenticate", {
launchId, state: challenge.state
});
if (authorization.state !== challenge.state) throw new Error("State mismatch");
const response = await fetch("/zetapad/login", {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: authorization.code,
state: authorization.state })
});
if (!response.ok) throw new Error("App login failed. Authenticate again.");
const member = await response.json();
if (typeof member.userId !== "string" || !member.userId || !Array.isArray(member.scopes)) {
throw new Error("App backend did not return a verified member");
}
// IDs and profile fields come from the backend after verified REST login.
// Protected server actions must resolve userId from the session, not the body.
return { member };
}
enterRoom().then(startYourApp).catch(showLoginError);
Server requirements
GET /zetapad/state returns { "state": "..." } for the current visitor's server-side session. POST /zetapad/login receives { "code": "...", "state": "..." } and returns the verified member only after state validation, code exchange, and userinfo read.
Those routes belong to your app. Do not return secrets or bearer tokens to the browser. Add framework-appropriate error handling and a supported embedded session mechanism.
This example returns the verified member for UI use. Returning IDs to your UI is optional; protected backend routes must resolve member identity through the server session even if the browser sends an ID in a request.
For local Zetapad at http://localhost:3000, replace the trusted origin list with ["http://localhost:3000"]. Production uses the fixed origins in the example.
Protect direct access
The top-level check prevents browser startup outside an iframe. Apply the document guard and backend access policy as well: this browser check cannot protect APIs or private data by itself.