Embed tokens are self-verifiable signed JWTs carrying a resource type, a resource id, locked params and an expiry, signed with a per-organisation key. The render surface takes no authentication. And because Qrly is priced on revenue rather than per viewer, embedding to ten thousand end users does not change the bill.
They are deliberately not one abstraction. Each has its own trust model, and choosing wrongly is the usual cause of an embedded-analytics security incident.
{}The claim set is small on purpose. Everything a viewer is allowed to do is decided at issue time, on your server, and then signed.
{} — the claim is never simply absent/embed/{token} — the token is the credentialpostMessage, re-broadcast as a DOM eventThe SDK is served from the application itself — there is no CDN dependency and no external asset request from an embedded page.
Your application already knows who the viewer is. Issue the token there, with the tenant identifier locked in, and hand the browser nothing else. This is the whole of the multi-tenant story.
curl -X POST https://bi.example.com/api/embed/tokens \
-H "Authorization: Bearer $QRLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "DASHBOARD",
"resourceId": 412,
"lockedParams": { "tenant_id": "acme-gmbh" },
"expiresInSeconds": 900
}'
Qrly.init takes the base URL of your installation, the token you just minted and a theme. Qrly.embed takes a selector and either a questionId or a dashboardId, plus any params that are not locked. The iframe reports its own height back over postMessage, which the SDK re-broadcasts as a DOM event so your layout code can react without listening on the raw message channel.
<div id="usage-dashboard"></div>
<script src="https://bi.example.com/embed/qrly-sdk.js"></script>
<script>
Qrly.init({
baseUrl: 'https://bi.example.com',
token: EMBED_TOKEN, // minted server-side, short-lived
theme: 'dark'
});
Qrly.embed('#usage-dashboard', {
dashboardId: 412,
params: { period: 'last_30_days' } // viewer-supplied, never security-bearing
});
// iframe auto-resize is re-broadcast as a DOM event
document.addEventListener('qrly:resize', (e) => {
console.log('embed height', e.detail.height);
});
</script>
Anything in lockedParams is covered by the signature and is decided by your server. Anything in params is supplied by the browser and must be treated as untrusted — it is for periods, groupings and presentation, not for deciding whose rows come back. Qrly applies the same discipline to dashboard filters generally: a viewer can only supply a filter id and a value, never a column or an operator, and the binding operator comes from an allow-list.
Every embedded-analytics vendor can put an iframe in your product. Qrly puts a constrained agent in it — with allow-lists and caps written into the token rather than trusted to the front end.
The admin page issues an embed-agent token and hands back a copyable snippet. The token, the endpoint and the container element are the entire integration surface.
<div id="qrly-agent"></div>
<script
src="https://bi.example.com/embed/qrly-agent.js"
data-token="<embed-agent token>"
data-endpoint="https://bi.example.com"
data-container="#qrly-agent">
</script>
All of the following live inside the signed token, which has its own signing key and its own audience — separate from the question and dashboard embed keys, so revoking one does not disturb the other.
Note what is not in that list: no way for the embedded page to widen its own scope. The allow-lists are claims in a signed token, and a browser that edits them invalidates the signature.
Written for the engineer who has already priced an embedded-analytics vendor once and did not enjoy it.
The multi-tenant SaaS case is concrete. You build one dashboard against one connection, parameterised on the tenant column. You then mint one embed token per customer with {"tenant_id": "…"} in lockedParams. Every customer opens the same dashboard id and sees only their own rows, because the scoping is in a claim your server signed, not in a query-string value their browser could edit.
This scales the way you want it to: adding your five-hundredth customer is minting a five-hundredth token, not authoring a five-hundredth dashboard. And when you change the dashboard, all five hundred customers get the change at once, because there is only ever one of it.
Embedded analytics is where per-seat pricing turns hostile. The people looking at the embedded dashboard are your customers' users — a population you do not control, cannot cap and are not going to charge individually. A per-viewer licence turns your own growth into a variable cost, and finance eventually asks you to gate the feature to slow it down.
Qrly is priced on revenue rather than on seats or viewers. The bill responds to how big your company is, not to how many people opened a chart last Tuesday. Embedding therefore stays a product decision rather than a licensing negotiation, and you never end up hiding analytics behind a plan tier for reasons that have nothing to do with your customers.
Putting a chatbot with database access into a customer-facing product is normally the point where security review stops the project — and reasonably so, because the usual design puts the guardrails in the front-end code that the customer's browser is running.
Qrly's embed-agent token carries the allow-lists itself: agent type, allowed connections, allowed questions, allowed tools, locked params, per-session message and tool-call caps, a per-day session cap. Those are signed claims verified server-side on every call. The reviewable question stops being "what could the widget be talked into" and becomes "what did we put in the token", which is a question with a written answer.
Row-level security and data sandboxing as general platform features are not implemented in Qrly. There is no policy language that filters every query for every user everywhere in the product. If your evaluation depends on that, you should know it now rather than in week three of a proof of concept.
What ships is narrower and, for the embedded case, sufficient: locked params in a signed token, and the embed-agent allow-lists. Inside the application, access is governed by collection, data and project permission levels resolved most-permissive-wins across a user's groups, plus always-applied model filters on OLAP models, which act as a security boundary for model-driven queries. Those are different mechanisms with different reach, and we would rather name them than blur them into a marketing word.
Locked params. An embed token is a signed JWT carrying the resource type, the resource id, a locked-params map and an expiry. The locked params are baked into the token at issue time and cannot be overridden by anything the browser sends, because the signature covers them. You issue one token per customer with their tenant identifier locked in, and every customer gets the same dashboard scoped to their own rows.
No. Row-level security and data sandboxing as a general platform feature are not implemented in Qrly. The shipped equivalents are embed locked params and the embed-agent allow-lists, which cover the embedded multi-tenant case specifically. For in-application access control Qrly uses collection, data and project permission levels resolved across groups. We would rather say this plainly than let you discover it during a proof of concept.
Nothing. Qrly is priced on revenue, not on seats or viewers. Embedding a dashboard to ten thousand end users of your product does not change the bill, which is the difference between embedded analytics being a product decision and embedded analytics being a per-viewer finance negotiation.
No. The public render surface at /embed/{token} takes no authentication at all — the token is the credential and it verifies itself. Your application issues the token server-side with your own session already established, and the browser only ever sees a short-lived signed value scoped to one resource.
Every embed token carries an expiry. The default lifetime and the maximum lifetime are both configurable per installation, so an administrator can cap how long any token may be minted for. Tokens can also be listed and revoked individually from the admin surface, and each organisation signs with its own key — the placeholder key that ships with the distribution is rejected outright.
A drop-in script tag that puts a Qrly AI agent inside your own product. Its token type carries the agent type, allowed connections, allowed questions, allowed tools, locked params, per-session message and tool-call caps, a per-day session cap, a greeting message, a brand logo and colour, its own signing key and audience, an active flag and a revoked timestamp. The admin page issues one and hands back a copyable snippet.
Signed tokens, locked params, a constrained agent widget — and no per-viewer line item. Self-hosted. Made in Belgium.