(Tutorial) Hosting Apache Superset part 2: Configuring Superset for embeds
7 Aug 2025
Some weeks ago I wrote a tutorial on how to get Apache Superset hosted and running. In this, part two, I'll look at how to configure it specifically for embed usage into your own app.
Superset embeds work by generating short-lived embed tokens, called "guest tokens", by authenticating with Superset on your back end via the Superset API. The generated token is then used by the SDK to bring in the embed, as we'll see.
Be sure to check out part one first, as we'll be modifying and extendin the code we wrote in that tutorial.
1. Config tweaks 🔗
We'll need to make a few additions and tweaks to our config.py
file. Modify the `FEATURE_FLAGS` dict to include a new flag, EMBEDDED_SUPERSET
. Without this, Superset blocks any embed attempts.
FEATURE_FLAGS = {
**DEFAULT_FEATURE_FLAGS,
"ENABLE_TEMPLATE_PROCESSING": True,
"ENABLE_JAVASCRIPT_CONTROLS": True,
"EMBEDDED_SUPERSET": True # <-- the new bit
}
Next we need to configure guest tokens, which are used to authenticate embd requests. Add the following:
# guest tokens (for embeds)
GUEST_ROLE_NAME = "Gamma"
GUEST_TOKEN_JWT_SECRET = environ["GUEST_TOKEN_JWT_SECRET"]
GUEST_TOKEN_JWT_ALGO = "HS256"
GUEST_TOKEN_JWT_EXP_SECONDS = int(environ["GUEST_TOKEN_JWT_EXP_SECONDS"])
The role name, "Gamma", is a predefined role within Superset that grants read-only access. In the context of embeds, it's a baseline permission that is further limited by what we impose in the guest token (i.e. access only to the dashboard being embedded.) The other three settings are fairly self-explanatory; we'll pass in the two environment values when we modify the creation command in step 2.
Next, let's disable CSRF protection. This is because CSRF concerns front-end only, but if we're using Superset via embeds only (i.e. your users are not logging into SS directly), then we don't need CSRF. In fact, it's more helpful to disable it; otherwise, our requests to the Superset API (to generate guest tokens) would have to pass a CSRF token, which is a headache we don't need.
Next up, becasue Superset is running on a separate domain from your app, we need to set same-site to none for the cross-domain embed to work.
If you're running Superset on HTTPS you'll also want to set SESSION_COOKIE_SECURE = True
.
Finally - and it's a bit of a biggie - we'll need to make some overrides to Superset's default Talisman config. Talisman is a security library that SS uses to control things like CSP (content security policy).
# allow iframe embeds
from superset.config import TALISMAN_CONFIG as TALISMAN_DEFAULTS
TALISMAN_ENABLED = True
merged_csp = dict(TALISMAN_DEFAULTS.get("content_security_policy", null))
merged_csp["frame-ancestors"] = [
"'self'",
"http://localhost:3000",
"https://stage.example.com",
"https://example.com",
]
TALISMAN_CONFIG = {
**TALISMAN_DEFAULTS,
"content_security_policy": merged_csp,
"frame_options": None, # Disable X-Frame-Options
}
What we're doing there is merging our overrides into SS' default Talisman settings. Be sure to customise the domains you'll use.
2. Command tweaks 🔗
Now onto the Azure CLI command we used to build the container. We'll need to add two new environment variables that our new config settings are expecting. At the end of the command, append `
then add the following lines, replacing {jwt-secret}
with whatever you like:
As before, if you're not in Powershell use \
not `
at the ends of lines.
3. Injecting custom JavaScript 🔗
One of the issues of cross-domain frame-based embeds is that the parent page can't talk to the child page. This is key because we'll have no way of sizing our frame according to its content height.
Let's get round this by injecting a custom JS file into Superset. First, create a file called custom.js
and give it the following content.
console.log('SS custom JS loaded');
const domains = [
'https://example.com',
'https://stage.examplecom',
'http://localhost:3000'
];
let domain;
addEventListener('message', evt => {
if (!domains.includes(evt.origin)) return;
switch (evt.data.id) {
case 'report-domain':
domain = evt.data.domain;
setInterval(
() => parent.postMessage({
id: 'report-height',
height: document.body.scrollHeight
}, evt.data.domain),
2000
);
break;
}
});
Basically we set up an event listener for the parent to tell us what domain it's running on. Our custom JS then responds, by sending its own message back to the parent, with the content height. We do this repeatedly, to accommodate changes to the SS UI.
We'll write the corresponding parent-side JS code in step 7.
Now, to bundle it into the container, add the following to your Dockerfile, right after the part where we copy over our config file.
# copy and inject custom JS file
COPY custom.js /app/superset/static/assets/custom.js
RUN SPA_FILE=/app/superset/templates/superset/spa.html && \
CUSTOM_SNIPPET='<script nonce="{{ csp_nonce() }}" src="/static/assets/custom.js"></script>' && \
sed -i "/{%\s*block tail_js\s*%}/a $CUSTOM_SNIPPET" "$SPA_FILE"
First we copy our file into the container. Then we shoehorn it into Superset's spa.html template file, which runs on all analytics pages. That page contains Jinja templates, so we need to insert our script tag into the "tail JS" template, which means extra scripts that load at the end of the body.
Note the nonce, which we need in order to comply with Superset's CSP, which limits JS files to only those with the correct nonce. We utilise Superset's csp_nonce()
helper to add this in. Without this, our script will be blocked.
If you're thinking this all sounds very hacky, you'd be right, and there's probably a better way of doing this. Indeed, Superset's own Github repo lists a file which, apparently, you can simply overwrite with your own JS and it'll be imported automatically. I spent many hours and could not get that file to take effect (and lots of other people couldn't, either.)
That's it for container tweaks! Now rebuild, tag and push your image (step 8 in the original article), then rebuild your container (step 9.)
4. Create an embed role + user 🔗
Now our container is configured to support embeds, let's login to Superset. Go to Settings > List roles.
Create a new role, "Embed", give it a name, and give it the "can grant guest token on SecurityRestAPI" permission. Then save.
Now create a new user, just for embeds. Go to Settings > List users and create a new user. Fill out all the basic details but also:
- Be sure to check the "is active" box
- Under Role, add the role you just created
- Choose a username and password and make a note of these for use on our back end
Now save.
5. Prepare a dashboard for embed 🔗
Next we need to prep a dashboard for embed usage.
Still in Superset, create a dashboard or go to one you've already created. Go to the dashboard, then hit the three dots menu to the right. In there, you'll see Embed dashboard. Click that, and fill out the modal form. You'll then be given a dashboard ID - make a note of that for the next step.
6. Generating guest tokens 🔗
Now attention turns to our back end, firstly to authenticate with Superset and then to generate a guest token, i.e. a token that authenticates the embed.
In your back end somewhere, create a new route as follows (I'm using Node + Itty Router, but adapt this to whatever setup you have.) This is the route our front end will call over XHR to obtain the guest token.
const exp = null
export default exp;
exp.embedToken = async req => {
//prep
let res, data;
const domain = 'http://{container-name}.{container-region}.azurecontainer.io:8088';
const dashboardId = '{dashboard-id}';
//login to SS and get access token for next request
const loginEndpoint = 'api/v1/security/login'
res = await fetch(`${domain}/${loginEndpoint}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: "{embed-user}",
password: "{embed-pass}",
provider: 'db'
})
});
data = await res.json();
const accessToken = data.access_token;
//generate ephemeral guest token for embed
const guestTokenEndpoint = `api/v1/security/guest_token`;
res = await fetch(`${domain}/${guestTokenEndpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
user: {
username: 'foo',
first_name: 'foo',
last_name: 'foo'
},
resources: [{type: 'dashboard', id: dashboardId}],
rls: [{clause: `TRUE`}]
})
});
data = await res.json();
return {token: data.token, dashboardId};
}
In the above, replace:
{container-name}
and{container-region}
with your container name and Azure region (e.g. "uksouth"){dashboard-id}
with the ID of your dashboard{embed-user}
and{embed-pass}
with the username and password of the embed user you set up in Superset
Obviously, in production you'll want to move your user/pass out to secrets rather than hard-code them in the code.
What's happening there is we make two consecutive requests: the first to login to Superset (against the embed user we created; provider: 'db'
tells Superset we're authenticating a user), and the second to obtain a guest token for our embed, against a specific dashboard ID.
It's that guest token our route spits out for our front end to grab.
7. Embed the dashboard 🔗
Lastly, to the front end. Firstly, let's install the Superset embed SDK:
Then, having made the XHR to our back end to obtain the token, we call the SDK's embedDashboard
method. We'll put the embed into an element with ID "ss-container".
import { embedDashboard } from "@superset-ui/embedded-sdk";
const container = document.querySelect('#ss-container');
const dash = embedDashboard({
id: {dashboard-id},
supersetDomain: '{container-url}',
mountPoint: container,
fetchGuestToken: () => '{embed-token}',
dashboardUiConfig: {
hideTitle: true,
filters: {
visible: false,
}
},
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'],
referrerPolicy: 'same-origin'
});
Swap out the {placeholder}
placeholders with the appropriate details.
Then, we'll want to size the frame according to its content height so we don't see any scrollbars. We'll do this via the post message API, which facilitates messages shared between frames on different origins.
First, disable scrolling on the frame:
Next let's set up a message event listener, for when the SS frame tells us what the content height is (this is done in our custom JS we bundled into the container.) We'll expect a message ID and the content height in the event data.
addEventListener('message', evt => {
if (evt.data?.id == 'report-height')
ifr.style.height = evt.data.height+'px';
});
Before this can work, though, we'll want to notify our custom JS (in the SS frame) what domain we're on. After all, the SS frame doesn't know if we're on dev, stage or production since it can't glean the parent's URL.
ifr.onload = () => ifr.contentWindow.postMessage(
{id: 'report-domain', domain: `${location.protocol}//${location.host}`},
'{container-domain}'
);
Replace {container-domain}
with the domain your SS is running on.
And that, ladies and gentlemen, is that!
Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!