cap-de-edit-embed

SKILL.md

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

  1. Your server authenticates the user and creates a JWT with identity, role, and routing attributes
  2. JWT is signed with a shared secret from Domo
  3. Server constructs edit URL: {IDP_URL}/jwt?token={jwt_token}
  4. Client renders URL directly as iframe src (no POST form needed)
  5. 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_ATTRIBUTE name is whatever you agreed on with your CSM (customer, keyAttribute, tenant). Values must match Domo's instance mapping exactly.
  • Secret management: Never expose JWT_SECRET client-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.

Weekly Installs
3
GitHub Stars
10
First Seen
Today
Installed on
windsurf3
amp3
cline3
opencode3
cursor3
kimi-cli3