Developer Center · Extend the platform
Embedded Pages — put your own tools in the admin.
Register your own https-hosted page and it becomes a permissioned menu item inside the SocialHub admin — a full-page, sandboxed iframe. Flash hands your page a short-lived, PII-free identity token so you can authorize the operator, without them logging in again.
Early access · behind the open-extensibility module
How it works
Four steps from your URL to a menu item.
Register your page
A workspace admin adds your https URL in Flash → Configure → Embedded Pages, with a menu label, an origin allowlist, and which roles/tier may open it.
Copy the signing secret
Flash shows a signing secret once (slotsec_…). Store it as a server secret in your app — you verify Flash's identity token with it.
Flash frames your page
The page appears as a sidebar menu item under Apps for allowed operators; opening it loads your URL in a sandboxed iframe with ?flash_context=<token> appended.
Verify + authorize
Read the token, verify it server-side (HS256), and gate your UI on the workspace id + operator role it carries.
The identity token
Short-lived, PII-free, signed with your secret.
On every render, Flash appends ?flash_context=<JWT> to your URL. It is an HS256 JWT signed with your page'ssecret (so you — not the platform — are the trust anchor), valid for at most 120 seconds, and it carries no member personal data — only the workspace id and the operator's opaque id + coarse role, so you can authorize.
{
"iss": "flash",
"aud": "nav.page",
"slotKey": "nav.page",
"ctx": {
"teamId": "team_...", // the workspace (tenant) — the tenant already owns this
"userId": "usr_...", // opaque operator id (NOT an email/name/phone)
"role": "org_admin", // coarse role: org_owner | org_admin | marketer | store_manager
"pageType": "nav.page",
"locale": "en"
},
"jti": "…", "iat": 1730000000, "nbf": 1730000000,
"exp": 1730000110 // <= 120s after iat
}Verify it
Trust the token, not the request.
Verify the signature, issuer (flash), audience (nav.page) and expiry on your server. Then authorize on the workspace id + role it carries — never on cookies (the iframe has no usable origin; see below).
// Node (jose). The secret is your page's signing secret, shown ONCE at registration.
import { jwtVerify } from "jose";
const SECRET = new TextEncoder().encode(process.env.FLASH_EMBED_SECRET); // slotsec_…
export async function verifyFlashContext(token) {
const { payload } = await jwtVerify(token, SECRET, {
algorithms: ["HS256"],
issuer: "flash",
audience: "nav.page",
requiredClaims: ["exp", "jti"],
maxTokenAge: "120s",
});
// payload.ctx = { teamId, userId, role, pageType, locale }
return payload.ctx; // → authorize the operator with this
}// In your page: read ?flash_context=… , verify it server-side, then authorize.
const token = new URLSearchParams(location.search).get("flash_context");
// POST it to YOUR backend, verify with the snippet above, and gate the UI on
// ctx.teamId + ctx.role. Never trust it unverified, and never rely on cookies —
// the iframe is a null origin (see "Framing" below).Framing & security
What your page must do.
Allow the frame
Your page must permit being framed by flash.socialhub.ai — don't send X-Frame-Options: DENY; set Content-Security-Policy: frame-ancestors https://flash.socialhub.ai.
Assume a null origin
Flash sandboxes the iframe with allow-scripts and no allow-same-origin, so your page runs at a null origin: no Flash cookies, storage or parent DOM. Authenticate with the token, not cookies.
postMessage is allow-listed
The host only accepts postMessage from the origins you registered, and only from your iframe window. Keep your allowed-origins list tight and https-only.
Rotate the signing secret from the manager if it is ever exposed (old tokens stop verifying immediately). Embedded Pages is early access, off by default per workspace, and enabling it for production is subject to SocialHub's third-party-embed security review.
Match the admin look
Make it feel native, not bolted on.
Your page renders insidethe SocialHub admin, so it should look like it belongs. The admin is a single flat, light theme built on a few tokens — mirror them. Flash already wraps your page in a titled window with a “Third-party” badge, so don't add your own top bar, sidebar or full-page chrome; just render the content.
/* Starter tokens that match the SocialHub admin (flat, light, single-accent).
Drop into your embedded page so it feels native, not bolted on. */
:root {
/* type — the admin's three families */
--font-sans: "Inter", -apple-system, "Segoe UI", Roboto, sans-serif;
--font-display: "Plus Jakarta Sans", "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
/* surfaces — light theme only (the admin has no dark mode) */
--bg: #fafbfc; /* page background */
--surface: #ffffff; /* cards / panels */
--subtle: #f7fafc; /* muted fills */
--border: #e2e8f0; /* hairlines */
/* text */
--text: #121c3d; /* primary (brand navy) */
--text-2: #4a5568; /* secondary */
--muted: #64748b; /* muted */
/* accent — the admin's primary action colour */
--accent: #2ecc87;
--accent-dark: #25b678;
/* radius + 4px spacing scale + elevation */
--radius: 8px; --radius-lg: 12px;
--space-2: 8px; --space-3: 12px; --space-4: 16px; --space-6: 24px;
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
}
body { margin: 0; background: var(--bg); color: var(--text);
font: 14px/1.5 var(--font-sans); }
h1, h2, h3 { font-family: var(--font-display); color: var(--text); }
.card { background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
padding: var(--space-4); }
.btn { background: var(--accent); color: #fff; border: 0; font-weight: 600;
border-radius: var(--radius); padding: 8px 14px; cursor: pointer; }
.btn:hover { background: var(--accent-dark); }
code { font-family: var(--font-mono); color: var(--text-2); }No chrome of your own
Flash provides the window frame + badge. Skip your app's global nav/sidebar/header — render just the tool.
Light, flat, one accent
Light theme only (the admin has no dark mode): white cards on a #fafbfc page, hairline #e2e8f0 borders, one green accent (#2ecc87).
Fill the width, flow the height
Use the full width and let content flow; the host sizes the frame (roughly full viewport height) and scrolls — don't force your own 100vh or fixed footers.
Degrade gracefully
The fonts (Inter / Plus Jakarta Sans / JetBrains Mono) fall back to system-ui if you don't self-host them; honour prefers-reduced-motion and prefers-color-scheme: light.
These values track the current admin theme; treat them as a starting point and keep your own brand where it matters. The goal is a page that reads as one surface with SocialHub, not a window into a different product.
A complete example
A working embedded page in ~30 lines.
A minimal Express server you can run and register as-is: it lets Flash frame it, verifies the identity token, authorizes on the operator's role, and renders a tool scoped to the workspace. Copy the signing secret from the manager into FLASH_EMBED_SECRET and run it.
// server.js — a complete, minimal SocialHub Embedded Page (Express + jose).
// Register https://<host>/embed in Flash → Settings → Embedded Pages, allow the
// origin https://<host>, pick roles/tier, then copy the signing secret into env.
import express from "express";
import { jwtVerify } from "jose";
const app = express();
const SECRET = new TextEncoder().encode(process.env.FLASH_EMBED_SECRET); // slotsec_…
const FLASH = "https://flash.socialhub.ai";
app.get("/embed", async (req, res) => {
// 1. Let ONLY Flash frame this page.
res.setHeader("Content-Security-Policy", "frame-ancestors " + FLASH);
// 2. Read + verify the identity token Flash appended (?flash_context=…).
let ctx;
try {
const { payload } = await jwtVerify(String(req.query.flash_context || ""), SECRET, {
algorithms: ["HS256"], issuer: "flash", audience: "nav.page",
requiredClaims: ["exp", "jti"], maxTokenAge: "120s",
});
ctx = payload.ctx; // { teamId, userId, role, pageType, locale }
} catch {
return res.status(401).send("<p>Unauthorized — open this from inside SocialHub.</p>");
}
// 3. Authorize on the VERIFIED role (never on the raw request).
const canEdit = ctx.role === "org_owner" || ctx.role === "org_admin";
// 4. Render your tool, scoped to ctx.teamId. (Values are verified + opaque;
// still escape anything user-supplied you add.)
res.send(
'<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:24px">' +
'<h1>Returns portal</h1>' +
'<p>Workspace <code>' + ctx.teamId + '</code> · operator <code>' + ctx.userId +
'</code> · role <b>' + ctx.role + '</b></p>' +
(canEdit ? '<button>Process a return</button>' : '<p>Read-only for your role.</p>') +
'</body>'
);
});
app.listen(3000, () => console.log("Embedded page on :3000/embed"));1 · Allow the frame
frame-ancestors https://flash.socialhub.ai — so only the SocialHub admin can iframe it.
2 · Verify the token
Read ?flash_context, verify HS256 + issuer flash + audience nav.page + ≤120s expiry; reject otherwise.
3 · Authorize
Gate features on the VERIFIED role (org_owner / org_admin / …), never on the raw request.
4 · Render, scoped
Render your tool for ctx.teamId — the workspace it was opened in.
Then in Flash → Settings → Embedded Pages: add the URL (e.g. https://tools.yourapp.com/embed), list that same origin, choose the roles + tier that may open it, and paste the one-time signing secret into your server's environment. It now appears under Apps for the operators you allowed.