Embedded Analytics

Embed a question, a dashboard, or an AI agent

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.

Embed tokens

Signed, scoped, expiring — for a question or a dashboard

  • Self-verifiable signed JWT — no server-side session lookup
  • Carries resource type, resource id, locked params and an expiry
  • Locked params are always emitted, defaulting to {}
  • Signed with a per-organisation key
  • The shipped placeholder key is rejected
  • Configurable default and maximum token lifetimes
  • Create, list and revoke from the admin surface
Public share links

The simpler mechanism, for a report

  • A share token mints an unauthenticated viewer URL
  • Separate from embed tokens, with its own viewer route
  • Its own spreadsheet route for spreadsheet reports
  • Generate, copy, revoke — from report settings
  • Dashboards have their own public share link too
  • No locked params, no SDK, no parameter surface
Agent widget

An AI agent as a drop-in script tag

  • Token, endpoint and container — one script tag
  • Token carries agent type and locked params
  • Allow-lists for connections, questions and tools
  • Per-session message and tool-call caps
  • Per-day session cap
  • Greeting message, brand logo and colour
  • Its own signing key and audience, active flag, revoked timestamp
Claim or control
Shipped Qrly embed token Self-verifiable signed JWT
Where it is enforced And what it means in practice
Resource type
Question or dashboard
A question token cannot render a dashboard
Resource id
One resource, per token
Signature covers it — no id substitution
Locked params
Always emitted
Defaults to {} — the claim is never simply absent
Expiry
On every token
Configurable default and maximum lifetime
Signing key
Per organisation
One organisation cannot mint another's tokens
Distribution placeholder key
Rejected
You cannot ship to production still using it
Render surface authentication
None required
/embed/{token} — the token is the credential
Qrly attribution in the frame
Hideable
A brand flag hides "powered by" in embeds
Iframe height management
Auto-resize
postMessage, re-broadcast as a DOM event
Per-viewer licence cost
€0
Qrly is priced on revenue, not on seats or viewers
Row-level security / data sandboxing
Not implemented
Locked params + embed-agent allow-lists are the shipped equivalent
Legend Shipped Partial / equivalent mechanism Not implemented

Mint the token on your server

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
      }'

Render it in your page

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>

The rule that matters

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.

One script tag, three attributes

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>

What the agent token carries

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.

  1. Agent type. Which of Qrly's agent personas the widget runs as. The embedded widget is not a general-purpose console.
  2. Allowed connections. An explicit list. A connection that is not named cannot be reached, whatever the conversation asks for.
  3. Allowed questions. An explicit list of saved questions the agent may run.
  4. Allowed tools. An explicit list drawn from the agent runtime's tool set. Anything not listed is unavailable for that widget.
  5. Locked params. The same mechanism as dashboard embedding — the customer scoping is signed in, not negotiated at runtime.
  6. Per-session message cap and tool-call cap. A bounded conversation and a bounded amount of work per conversation.
  7. Per-day session cap. Bounded exposure per token, per day, which is what makes a public-facing widget a defensible cost.
  8. Greeting message, brand logo and brand colour. So the widget reads as part of your product rather than as a visitor from ours.
  9. Active flag and revoked timestamp. Turn one widget off without touching the others.

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.

01 / Multi-tenant

One dashboard, every customer, their rows only

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.

02 / Pricing

Ten thousand end users does not change the bill

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.

03 / The agent

Constraints in the token, not in the front end

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.

04 / Honesty

What the locked-param mechanism is not

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.

How does Qrly stop one embedded customer seeing another customer's rows?

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.

Does Qrly support row-level security?

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.

What does embedding cost per end user?

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.

Does the embedded page require my users to log in to Qrly?

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.

How long do embed tokens live?

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.

What is the embeddable AI agent widget?

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.

Ship analytics inside your product

Signed tokens, locked params, a constrained agent widget — and no per-viewer line item. Self-hosted. Made in Belgium.