Skip to content
datasquares.aiSign inStart free

Share & Export

Embed dashboards for each person

An embed app puts DataSquares dashboards and reports inside your own product and shows each person their own rows. It works with two keys:

  • The company key. A company administrator makes an embed app under Administration → Embedding. It has a client id and a key (a key id and a secret). The secret is shown once and stays on your server: it never reaches a browser.
  • The person’s id. For each person, your server signs a short-lived token with that key, naming the person, their tenant, their role, the values their rows are filtered by and the dashboard or report. The browser carries only that token. DataSquares checks it and swaps it, once, for a session bound to your site.

Every view and every Ask AI question is then counted for the person and for their tenant.

You choose when you make the app, and it can’t be changed afterwards: make another app for the other kind.

Internal (Business) Customer-facing (Enterprise)
Who sees it People in your company who are DataSquares users Your own customers, who are not DataSquares users
The token’s sub The person’s DataSquares email or SCIM id Your own stable id for the person; no email is needed
Their rows Their own row-level security and masking, exactly as when signed in The app’s row rules, from the token’s attributes
Ask AI Their own AI credits and your AI settings for their role Your company’s bought AI credits, never below zero
How it is priced Their seats; no one is added By views: 20,000 a month included

An internal person must be a member of the app’s workspace (or a company administrator), active, and matched by exactly one SCIM id or email. They open only what DataSquares itself would open for them: a dashboard their role may view or that is shared with them (or a group of theirs), a report their role may view, a published presentation they have a role on. No user is ever created, and each internal person is one person however a token names them (email or SCIM id). An internal token carries no attributes.

A customer whose sub happens to be a member’s email is another person: customers and people in your company are counted apart, and never share a session, a view or a question.

  1. Open Administration → Embedding and choose New embed app.
  2. Name it and choose Who it is for.
  3. Tick What it may show: the dashboards, reports and published presentations of this workspace it may open. A token can name only these. Find a dashboard, report or presentation searches them by name; what you have ticked stays listed whatever you search. A presentation is shown in present mode, never its editor, without Ask AI (see Links, embedding and kiosks).
  4. List the Sites it may be shown on, one per line: portal.example.com, or *.example.com for its subdomains (not example.com itself). A session works only on the site it was opened on. Every site (*) is not allowed.
  5. For a customer-facing app, write its Row rules, one per line as attribute = field, such as account = account_id (see Each person’s rows). With none, the form says that every person sees every row the view shows, and makes the app only once you tick Show every person every row.
  6. Choose whether to Offer Ask AI to people whose token allows it: the people whose token’s role is viewer_ai, which your server sets in the token (it is not a seat). Choose any monthly AI credit budget per person and per tenant and the questions allowed an hour (20 a person and 200 a tenant unless you change them).
  7. Choose Make the app. Copy the client id, the key id and the secret now: the secret is not shown again. Keep it in your server’s secret store.

Each app card shows its client id, its keys by their last four characters with when each was made and last used, what it shows (a dashboard deleted since says so), its sites, its rules and its Ask AI settings. Edit changes everything but who it is for. Taking a dashboard, a report or a site off the app ends the sessions on it at once, for good: putting it back opens none of them again.

An app has two keys at most, so you can rotate without a gap: choose New key, move your server to the new key, then Revoke the old one. A revoked key signs nothing more, and the sessions it made end at once. Revoke app ends every session of the app, and the app makes no new one.

The token is a JWT signed with HMAC-SHA256 (HS256), valid ten minutes at most, used once.

Part What it holds
Header alg: HS256 (nothing else is accepted); kid: the key id
iss The app’s client id
aud datasquares (or a list that includes it)
sub The person: your stable id for them (internal: their DataSquares email or SCIM id), up to 255 characters
tenant Their customer account, for a product that serves many companies (optional)
role viewer, or viewer_ai for a viewer who may use Ask AI (absent: viewer)
attributes The values their rows are filtered by, one for each of the app’s row rules: text, a number, true or false, or a list of text and numbers. A name is letters, digits, _, . and -; __proto__, constructor and prototype are refused
content { "type": "dashboard" | "report" | "presentation", "id": "<the id>" }, one of the app’s content
iat, exp When it was signed and when it expires, in seconds; ten minutes apart at most
jti A nonce, 8 to 128 characters: a token makes one session

In Node, the React SDK’s server entry signs it with Web Crypto, on Node 20 or later and edge runtimes:

// On YOUR server — never in a browser bundle
import { signEmbedToken } from '@digisquares/datasquares-embed-react/server'
app.get('/analytics-token', requireSignedIn, async (req, res) => {
const token = await signEmbedToken({
clientId: process.env.DS_EMBED_CLIENT_ID!,
keyId: process.env.DS_EMBED_KEY_ID!,
secret: process.env.DS_EMBED_SECRET!,
sub: req.user.id,
tenant: req.user.accountId,
role: req.user.canAskAi ? 'viewer_ai' : 'viewer',
attributes: { account: req.user.accountId },
content: { type: 'dashboard', id: 'your-dashboard-id' },
})
res.type('text/plain').send(token)
})

Any JWT library works the same way, for example jsonwebtoken:

const token = jwt.sign(
{ iss: clientId, aud: 'datasquares', sub, tenant, role: 'viewer_ai', attributes, content, jti: crypto.randomUUID() },
secret,
{ algorithm: 'HS256', keyid: keyId, expiresIn: '5m' },
)

With React, pass getToken; the component asks your server for a token when the embed starts and again before its session ends, and hands it over by postMessage to the exact DataSquares origin, never in a URL. See the React embed SDK.

<DataSquaresEmbed baseUrl="https://app.datasquares.ai" getToken={() => fetch('/analytics-token').then((r) => r.text())} />

Without React, put the signed token in the address’s fragment, which a browser never sends to a server:

<iframe src="https://app.datasquares.ai/embed/session#token=SIGNED_TOKEN" width="100%" height="600" frameborder="0" allowfullscreen></iframe>

The embed reads the token once and clears it from the address. A plain iframe’s page can’t hand over a new token: when its session ends (or the page is reloaded without the token), the embed asks for one, waits ten seconds, then says the session has ended; reload the page with a new token. An iframe whose page answers the embed’s ds:token-request message with ds:token within those ten seconds (as the SDK does) keeps going, and a token that comes later still starts it.

A session lasts an hour and works only on the site it was opened on, for every request, the one that reads what its token stands for (GET /api/shares/{token}) included. Revoking the app or the key that made it ends it; so does taking its dashboard, report or site off the app, deleting its dashboard or report, and an internal person leaving the workspace, being deactivated, or no longer being able to open the content (their role changed, or a share was taken back). A session that has ended answers 401 EMBED_SESSION_EXPIRED, and the embed asks its page for a new token. Theme, toolbar and parameter options (theme, toolbar, p_<id>) work in the address as for a share link embed.

Customer-facing. The app’s row rules decide the rows:

  • Every person’s token must carry an attribute for every rule, and an attribute without a rule is refused, so access fails closed: a token that does not fit the rules gets no session.
  • Each rule filters the model field it names to the person’s value, or to any of their values for a list. The filter applies after every other filter, so a filter, a slicer or a drill can narrow the rows but never widen them.
  • A card whose model has no field of that name, or two, is not shown. Name the same field in every model the app’s content uses.
  • A card built on a saved SQL query is not shown to a person whose rows the rules filter: its SQL has no field to filter.
  • The model’s own row-level security applies too. A person in an embed is not a member of your company and has no role, so a policy for everyone applies to them and a policy aimed at a role shows them none of that table’s rows; labelled fields stay masked.
  • An app with no row rules shows every person every row the view shows, narrowed only by that row-level security: the form says so and makes it only once you tick Show every person every row.

Internal. Each person reads as themselves: their role, their row-level security and their masking, exactly as when they sign in, and only content their role or a share opens for them.

In either kind of embed, saved AI output is not shown: AI summaries saved on cards, AI snapshot cards and a report’s AI commentary were written from their author’s rows, which the person may not see in full. A snapshot card and a commentary say so in their place; a live card shows each person their own rows.

When the app offers Ask AI, a person whose token’s role is viewer_ai gets the Ask AI panel on the view. It answers only from that view’s cards, through the same filtered data as the embed, and your AI controls decide as for any AI: AI and Ask AI on shares and embeds switched on in the workspace, and the workspace’s budget.

  • A customer’s questions are paid from the AI credits your company bought, and stop at zero: they never go below zero. The app’s monthly budgets pause Ask AI for one person, or for one tenant, until the first of the next month (UTC); the view keeps working. Questions are counted an hour per person and per tenant. The answers run at your company’s viewers’ level.
  • An internal person asks as themselves, from their own credits, under your AI settings for their role.
  • History: each person finds their own conversations on the view again in their next session, and nobody else’s.
  • A person sees AI credits only: refusals name no amount, model or provider.

A view is one person on one embedded dashboard or report through one app for up to 30 minutes, with all its filters, drills and pages, the way analytics embedding is commonly counted: opening the content counts one when the person has none of it open. Reading what a session’s token stands for opens nothing. Internal embedding’s views are counted but never billed.

Enterprise includes 20,000 customer-facing views a month. Past them, views go on the next invoice at $50 (€45, ₹4,150) per 1,000, plus GST in India, without cutting anyone off: after the month ends and settles, an invoice of its own carries one line, Embedded views past the 20,000 included, with the views and the rate. The workspace’s administrators get the month’s numbers in the app. See Embedded views on the billing page.

Administration → Usage → Embedded use shows this month’s customer-facing views against the 20,000 included, and the last 30 days by app, by tenant and by person: views, questions and AI credits, in whole credits, the busiest first. The tenants and the people each page on their own, 20 at a time, or 50 or 100. An internal person shows as the user they are.

Erase a person. When one of your customers asks you to delete their data, choose Remove beside them under By person and confirm. Their sessions end at once and their Ask AI conversations are deleted; their views and questions stay counted, without them, so the month’s numbers do not change. A token your server signs for the same id later makes a new person. People in your company have no Remove: their embedded use is erased with the rest of their data.

The audit log records embed.session_started, embed.viewed and embed.question_asked with the app, the tenant and the person, and embed_app.created, embed_app.updated, embed_app.revoked, embed_app.key_created, embed_app.key_revoked and embed_person.erased with the administrator who did it.

POST /api/embed/sessions refuses with one of these codes; the embed tells your page with its ds:error event.

Code Why
401 EMBED_TOKEN_INVALID The token itself. details.reason says which check: malformed, algorithm, unknown_key (a revoked key or app too), signature, issuer, audience, expired, not_yet_valid, lifetime, claims, attributes (an attribute that is not a name with a plain value, __proto__, constructor and prototype included) or replayed (its nonce was used)
403 EMBED_SITE_REFUSED The page is not on one of the app’s sites, or does not say where it is (a no-referrer policy around the iframe)
403 EMBED_CONTENT_REFUSED The content is not one of the app’s, or, for an internal app, the person’s role does not open it and it is not shared with them
403 EMBED_ATTRIBUTES_REFUSED The attributes do not fit the row rules: details.missing and details.unknown name them. Internal tokens carry none
403 EMBED_PERSON_UNKNOWN An internal token’s sub matches no one member of the workspace
403 EMBED_APP_OWNER_GONE The app’s maker left; a company administrator opens it and saves it to take it over
403 PLAN_TIER_REQUIRED The workspace’s plan no longer includes the app’s kind: details.feature is internal_embedding or customer_embedding
  • Stories can’t be embedded yet: an app shows dashboards and reports.
  • An embed has no export yet, so no export counts as a view.
  • There is no scheduled delivery to an embedded person, so none counts as a view.
  • Data requests from an embed are limited per address as for share links; Ask AI is also limited per person and per tenant.
  • The browser’s framing policy for a session embed is not narrowed to the app’s sites; the session itself refuses every other site.