cap-de-edit-embed
Domo Embedded Edit Experience
Let external users create, edit, save, and share Domo content (dashboards, cards, alerts, reports, data sources) through an embedded iframe. Uses the Domo Identity Broker with JWT auth — fundamentally different from read-only embeds.
For read-only embeds, see cap-de-programmatic-filters. For client-side filtering, see cap-de-jsapi-filters.
How It Works
- Your server authenticates the user and creates a JWT with identity, role, and routing attributes
- JWT is signed with a shared secret from Domo
- Server constructs edit URL:
{IDP_URL}/jwt?token={jwt_token} - Client renders URL directly as iframe
src(no POST form needed) - Identity Broker validates JWT, routes user to correct Domo instance by role
Key difference: read-only = OAuth → embed token → POST form. Edit = JWT → Identity Broker URL → iframe src.
Prerequisites
Work with your Domo CSM to set up the Identity Broker. Provide: your Domo instance URL, auth method (JWT), routing attribute key, and attribute-to-instance mappings.
You receive: Identity Broker URL, JWT signing secret (UUID), and attribute key config.
Required env vars: IDP_URL (Broker URL), JWT_SECRET (signing secret), KEY_ATTRIBUTE (routing attribute name).
The Domo Identity Broker
Authenticates users (validates JWT signature) and routes them to the correct Domo instance based on the mapping attribute. Supports SAML2, OIDC, JWT, OAuth2 — JWT is most common for embeds.
Instance Mapping
Each mapping attribute value corresponds to a Domo instance:
| Attribute Value | Domo Instance |
|---|---|
acme-corp |
acme.domo.com |
globex |
globex.domo.com |
initech |
initech.domo.com |
Configured via your CSM (webform dataset or Excel). Comma-separated values route to multiple instances.
JWT Token Structure
Required Fields
| Field | Type | Description |
|---|---|---|
sub |
string | The user's identifier — typically their email address |
exp |
number | Expiration time (EPOCH timestamp). Keep this short (5 minutes recommended) |
jti |
string | Unique token identifier. Use a UUID v4 to prevent replay attacks |
{KEY_ATTRIBUTE} |
string or string[] | The routing attribute that maps the user to a Domo instance |
Common Optional Fields
| Field | Type | Description |
|---|---|---|
name |
string | Display name for the user in Domo |
email |
string | User's email address |
role |
string | Domo role: Admin, Privileged, Editor, or Participant |
employee_id |
string | Employee identifier |
title |
string | Job title |
department |
string | Department name |
location |
string | Location |
phone |
string | Phone number |
locale |
string | Locale preference |
timezone |
string | Timezone preference |
groups |
string[] | Group assignments within Domo |
Domo Roles
| Role | Capabilities |
|---|---|
Admin |
Full access — manage users, data, content, and settings |
Privileged |
Create/edit dashboards, cards, dataflows; manage data sources |
Editor |
Create/edit dashboards and cards; limited data source access |
Participant |
View and interact with shared content only (default if omitted) |
Most external users should be Editor or Participant.
Server-Side Implementation
Step 1: Create the JWT Token
Node.js / TypeScript:
import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";
function createEditToken(user: {
username: string;
email: string;
domoRole?: string;
mappingValue?: string | string[];
}) {
const jwtBody: Record<string, unknown> = {
sub: user.username,
name: user.username,
email: user.email,
role: user.domoRole || "Participant",
jti: uuidv4(),
};
// Add the routing attribute for instance mapping
const keyAttribute = process.env.KEY_ATTRIBUTE;
if (keyAttribute && user.mappingValue) {
jwtBody[keyAttribute] = user.mappingValue;
}
return jwt.sign(jwtBody, process.env.JWT_SECRET!, {
expiresIn: "5m",
algorithm: "HS256",
});
}
Python:
import jwt
import uuid
import time
import os
def create_edit_token(user):
payload = {
'sub': user['username'],
'name': user['username'],
'email': user['email'],
'role': user.get('domo_role', 'Participant'),
'jti': str(uuid.uuid4()),
'exp': int(time.time()) + 300 # 5 minutes
}
key_attribute = os.environ.get('KEY_ATTRIBUTE')
if key_attribute and user.get('mapping_value'):
payload[key_attribute] = user['mapping_value']
return jwt.encode(payload, os.environ['JWT_SECRET'], algorithm='HS256')
Step 2: Construct the Edit URL
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
Optionally deep-link to a specific page:
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}&destination=/page/${pageId}`;
Step 3: Build the API Route
Next.js App Router:
// app/api/editembed/route.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";
export async function POST(req: NextRequest) {
try {
const { embedID } = await req.json();
// Authenticate the user against your own auth system
const token = req.cookies.get("token")?.value;
if (!token) {
return NextResponse.json(
{ message: "Unauthorized: Please log in" },
{ status: 401 },
);
}
const user = await verifyAndGetUser(token);
if (!user) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
// Handle comma-separated mapping values
let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
mappingValue = mappingValue.split(",").map((s: string) => s.trim());
}
// Build the JWT payload
const jwtBody: Record<string, unknown> = {
sub: user.username,
name: user.username,
role: user.domoRole || "Participant",
email: user.email,
jti: uuidv4(),
};
const keyAttr = process.env.KEY_ATTRIBUTE;
if (keyAttr && mappingValue) {
jwtBody[keyAttr] = mappingValue;
}
// Sign the token
const editToken = jwt.sign(jwtBody, process.env.JWT_SECRET || "", {
expiresIn: "5m",
});
// Return the Identity Broker URL
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
return NextResponse.json(editUrl);
} catch (error) {
console.error("Error in /api/editembed:", error);
return NextResponse.json(
{ message: "Server error occurred" },
{ status: 500 },
);
}
}
Express example:
app.post("/api/editembed", authenticateUser, (req, res) => {
const user = req.user;
let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
mappingValue = mappingValue.split(",").map((s) => s.trim());
}
const jwtBody = {
sub: user.username,
name: user.username,
role: user.domoRole || "Participant",
email: user.email,
jti: uuidv4(),
};
const keyAttr = process.env.KEY_ATTRIBUTE;
if (keyAttr && mappingValue) {
jwtBody[keyAttr] = mappingValue;
}
const editToken = jwt.sign(jwtBody, process.env.JWT_SECRET, {
expiresIn: "5m",
});
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
res.json(editUrl);
});
Step 4: Render in an Iframe
Edit embeds load directly via iframe src (no POST form like read-only embeds):
function EditEmbed({ editUrl }: { editUrl: string }) {
return (
<iframe
src={editUrl}
style={{ width: "100%", height: "100%", border: "none" }}
allow="fullscreen"
/>
);
}
Handling Read-Only vs Edit Mode
If your app supports both read-only and edit modes, your embed component needs to handle both flows:
function EmbedDashboard({ embedID }: { embedID: string }) {
const [embedURL, setEmbedURL] = useState<string | null>(null);
const [embedToken, setEmbedToken] = useState<string | null>(null);
const isEditMode = embedID === "edit";
useEffect(() => {
const endpoint = isEditMode ? "/api/editembed" : "/api/getembedtoken";
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embedID }),
credentials: "include",
})
.then((res) => res.json())
.then((data) => {
if (isEditMode) {
setEmbedURL(data); // Edit returns a direct URL string
setEmbedToken(null);
} else {
setEmbedURL(data.embedUrl); // Read-only returns { embedUrl, embedToken }
setEmbedToken(data.embedToken);
}
});
}, [embedID, isEditMode]);
// Edit mode: direct iframe src
if (isEditMode && embedURL) {
return (
<iframe
src={embedURL}
style={{ width: "100%", height: "100%", border: "none" }}
/>
);
}
// Read-only mode: POST form submission to iframe
// (handle embedToken + form submission as in cap-de-programmatic-filters skill)
}
User Management for Edit Embeds
Key User Properties
| Property | Purpose | Example |
|---|---|---|
domoRole |
Controls edit capabilities | 'Editor', 'Participant' |
mappingValue |
Routes user to correct Domo instance | 'acme-corp' or ['acme-corp', 'globex'] |
email |
Required for Domo user identity | 'user@example.com' |
Multi-Instance Users
If a user needs access to multiple Domo instances, store their mappingValue as a comma-separated string or array:
// Single instance
user.mappingValue = "acme-corp";
// Multiple instances
user.mappingValue = "acme-corp, globex";
// or
user.mappingValue = ["acme-corp", "globex"];
When building the JWT, handle both formats:
let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
mappingValue = mappingValue.split(",").map((s) => s.trim());
}
Deep Linking
const editUrl = `${IDP_URL}/jwt?token=${editToken}&destination=/page/${pageId}`;
const editUrl = `${IDP_URL}/jwt?token=${editToken}&destination=/kpicard/${cardId}`;
Gotchas and Best Practices
- Token expiration: Keep JWTs short-lived (5 min recommended). Only used for initial auth — session persists after.
- Signing algorithm: Use
HS256. Domo expects this for the Identity Broker. - JTI uniqueness: Always UUID v4. Domo may reject reused JTI values (replay protection).
- Mapping values:
KEY_ATTRIBUTEname is whatever you agreed on with your CSM (customer,keyAttribute,tenant). Values must match Domo's instance mapping exactly. - Secret management: Never expose
JWT_SECRETclient-side. Use env vars or a secrets manager. - Security model: Read-only embeds scope to specific dashboards with filters. Edit embeds give a full Domo session per role — be conservative with role assignment.
- Instance mapping changes: Use stable values (tenant IDs, not company names that might change).
Environment Variables Reference
| Variable | Description | Example |
|---|---|---|
IDP_URL |
Identity Broker URL | https://yourcompany.identity.domo.com |
JWT_SECRET |
Shared signing secret (UUID format) | aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee |
KEY_ATTRIBUTE |
Attribute key for instance routing | keyAttribute, customer, tenant |
TypeScript Type Definitions
type DomoRole = "Admin" | "Privileged" | "Editor" | "Participant";
interface EditEmbedUser {
username: string;
email: string;
domoRole?: DomoRole;
mappingValue?: string | string[];
}
interface EditJwtPayload {
sub: string;
name?: string;
email?: string;
role?: DomoRole;
jti: string;
exp?: number;
[keyAttribute: string]: unknown; // dynamic routing attribute
}
Quick Reference
Read references/identity-broker.md for additional details on Identity Broker configuration and instance mapping management.