Skip to content

Embed SDK

The Querri Embed SDK lets you embed dashboards, projects, and the Library into any web application. It creates a secure, sandboxed iframe and handles authentication, theming, and communication automatically.

Zero dependencies. Vanilla JavaScript. ~4KB minified.

Terminal window
npm install @querri-inc/embed

Or load via script tag:

<script src="https://app.querri.com/sdk/querri-embed.js"></script>

The package exports two entry points:

ImportUse
@querri-inc/embed/serverServer SDK — Node.js backend, manages users/sessions/policies
@querri-inc/embed (script tag)Client SDK — browser, renders the embed iframe

The fastest way to get a user-scoped embed running:

1. Backend (Node.js)

import { Querri } from '@querri-inc/embed/server';
const querri = new Querri({
apiKey: process.env.QUERRI_API_KEY, // qk_...
orgId: process.env.QUERRI_ORG_ID, // org_...
host: 'https://app.querri.com',
});
// Create a session for an end user
app.get('/api/querri-token', async (req, res) => {
const session = await querri.getSession({
user: {
external_id: req.user.id, // your app's user ID
email: req.user.email,
first_name: req.user.firstName,
last_name: req.user.lastName,
},
});
res.json({ sessionToken: session.session_token });
});

2. Frontend (HTML)

<div id="querri-container" style="width: 100%; height: 600px;"></div>
<script src="https://app.querri.com/sdk/querri-embed.js"></script>
<script>
var querri = QuerriEmbed.create('#querri-container', {
serverUrl: 'https://app.querri.com',
auth: {
fetchSessionToken: async function () {
var resp = await fetch('/api/querri-token');
var data = await resp.json();
return data.sessionToken;
},
},
startView: '/library',
chrome: { header: { show: false } },
});
querri.on('ready', function () {
console.log('Querri embed loaded');
});
</script>

The SDK supports three authentication modes. Choose based on your use case:

Share KeyServer TokenPopup Login
User login required?NoNoYes
Server-side code needed?NoYesNo
Per-user data isolation?No (anonymous)YesYes
Row-level security?NoYesYes
Best forPublic dashboardsSaaS embedding, white-labelInternal tools

For public dashboards and reports that don’t require login. Generate a share key in Querri (Dashboard > Share > Create Link):

QuerriEmbed.create('#container', {
serverUrl: 'https://app.querri.com',
auth: {
shareKey: 'abc123...',
org: 'org_abc123',
},
startView: '/builder/dashboard/DASHBOARD_UUID',
});

Mode 2: Server Token (Enterprise / White-Label)

Section titled “Mode 2: Server Token (Enterprise / White-Label)”

For embedding Querri behind your own authentication. Your backend uses the server SDK to create a scoped session token:

QuerriEmbed.create('#container', {
serverUrl: 'https://app.querri.com',
auth: {
fetchSessionToken: async function () {
var resp = await fetch('/api/querri-token');
var data = await resp.json();
return data.sessionToken;
},
},
startView: '/library',
});

See Server SDK below for the backend implementation, including user provisioning, data upload, sharing, and row-level security.

For internal tools where users have Querri accounts. The SDK opens a popup for authentication and caches the token for return visits:

QuerriEmbed.create('#container', {
serverUrl: 'https://app.querri.com',
auth: 'login',
startView: '/home',
});
OptionTypeRequiredDefaultDescription
serverUrlstringYesYour Querri instance URL
authstring or objectYesAuthentication mode
startViewstringNonullInitial path (e.g., /library, /builder/dashboard/UUID)
chromeobjectNo{ sidebar: { show: false } }UI visibility options
themeobjectNo{}CSS custom property overrides
PathWhat it shows
/libraryThe user’s data sources (Library page)
/homeHome page
/builder/dashboard/UUIDA specific dashboard
/chat/UUIDA specific project’s chat
chrome: {
header: { show: false }, // Hide the top navigation bar
sidebar: { show: false }, // Hide the sidebar (default)
}
querri.on('ready', function () { /* Loaded and authenticated */ });
querri.on('error', function (data) { /* { code, message } */ });
querri.on('session-expired', function () { /* Auto-retries in Mode 2/3 */ });
querri.on('navigation', function (data) { /* { path } */ });
  • instance.on(event, callback) — register an event listener
  • instance.off(event, callback) — remove an event listener
  • instance.destroy() — remove the embed and clean up all resources

Each QuerriEmbed.create() call creates an independent instance. You can embed multiple views on a single page, each with its own auth and configuration.


The server SDK (@querri-inc/embed/server) manages the full lifecycle of embedded users: provisioning users, uploading data, creating access policies, sharing resources, and generating session tokens.

import { Querri } from '@querri-inc/embed/server';
const querri = new Querri({
apiKey: process.env.QUERRI_API_KEY, // qk_... key
orgId: process.env.QUERRI_ORG_ID, // org_... ID
host: 'https://app.querri.com', // or http://localhost for dev
});

You can also pass just the API key as a string: new Querri('qk_...'). The org ID falls back to the QUERRI_ORG_ID environment variable.

getSession() is the flagship method. It handles user provisioning, policy management, and session creation in a single call:

const session = await querri.getSession({
user: {
external_id: 'user-123', // your app's user ID
email: 'alice@example.com',
first_name: 'Alice',
last_name: 'Smith',
},
access: {
sources: ['SOURCE_UUID'], // data sources this user can see
filters: { region: 'US' }, // row-level filter
},
ttl: 3600, // session lifetime in seconds (default: 1 hour)
});
// session.session_token — pass to the client SDK
// session.user_id — Querri's internal user ID
// session.external_id — your external_id echoed back
// session.expires_in — seconds until expiry

What getSession() does internally:

  1. Creates or finds the user by external_id
  2. Hashes the access spec to generate a deterministic policy name
  3. Creates the access policy if it doesn’t exist, or finds the existing one
  4. Assigns the user to the policy
  5. Creates an embed session token
  6. Returns the token and user metadata

User shorthand — if you only need the external ID:

const session = await querri.getSession({ user: 'user-123' });

You can control what data the user sees in two ways:

Inline filters (recommended for most cases) — the SDK auto-manages the policy:

access: {
sources: ['source-uuid-1', 'source-uuid-2'],
filters: {
region: 'US', // single value
department: ['Sales', 'Marketing'], // multiple values (OR)
},
}

Pre-created policy IDs — when you manage policies yourself:

access: {
policy_ids: ['policy-uuid-1', 'policy-uuid-2'],
}

No access block — the user gets full access to all shared resources (no RLS filtering).

Upload CSV, Excel, JSON, or Parquet files programmatically:

import { readFileSync } from 'fs';
const file = new Blob([readFileSync('./data.csv')], { type: 'text/csv' });
const uploaded = await querri.files.upload(file, 'quarterly-report.csv');
console.log(uploaded.id); // source UUID — use this for policies and sharing
console.log(uploaded.name); // 'quarterly-report.csv'

The file is automatically processed (parsed, columnar storage created) after upload. It appears in the Library once processing completes.

After uploading, you need to share the source so embed users can see it. There are two approaches:

Organization-wide sharing — all users in the org can see the source:

// Not yet available in the SDK — use the API directly
await fetch('https://app.querri.com/api/v1/sources/SOURCE_UUID/org-share', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.QUERRI_API_KEY}`,
'X-Tenant-ID': process.env.QUERRI_ORG_ID,
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: true }),
});

Per-user sharing — share with specific users:

await querri.sharing.shareProject(projectId, userId, 'view');
await querri.sharing.shareDashboard(dashboardId, userId, 'view');

Access policies control which rows each user sees. If you use getSession() with inline access.filters, policies are auto-managed for you. But you can also manage them directly:

Create a policy:

const policy = await querri.policies.create({
name: 'US Region Only',
description: 'Only see US rows',
source_ids: ['source-uuid'],
row_filters: [{ column: 'region', values: ['US'] }],
});

Assign users to a policy:

await querri.policies.assignUsers(policy.id, {
user_ids: ['user-id-1', 'user-id-2'],
});

Verify what a user can see:

const resolved = await querri.policies.resolve(userId, sourceId);
console.log(resolved.where_clause); // "region" IN ('US')

Discover available columns (for building policy UIs):

const columns = await querri.policies.columns(sourceId);
// [{ source_id, source_name, columns: [{ name, type }] }]
  • Same column, multiple policies = OR. Policies for region = US and region = EU → user sees US or EU rows.
  • Different columns = AND. Policies for region = US and department = Sales → user sees only rows matching both.
  • No policies assigned = full access (permissive default).
// Create or find a user by external ID
const user = await querri.users.getOrCreate('ext-123', {
email: 'alice@example.com',
first_name: 'Alice',
last_name: 'Smith',
role: 'viewer',
});
// List users
const page = await querri.users.list({ limit: 50 });
for await (const user of page) {
console.log(user.email, user.external_id);
}
// Update a user
await querri.users.update(user.id, { role: 'editor' });
// Delete a user
await querri.users.del(user.id);

After creating a session, you can create a client that acts as the user. This is useful for server-side operations that should respect the user’s permissions:

const session = await querri.getSession({ user: 'ext-123' });
const userClient = querri.asUser(session);
// These calls are FGA-filtered — only returns what the user has access to
const projects = await userClient.projects.list();
const dashboards = await userClient.dashboards.list();
const sources = await userClient.data.sources();

If you need lower-level session control beyond getSession():

// Create a session directly
const session = await querri.embed.createSession({
user_id: 'user-id',
origin: 'https://myapp.com',
ttl: 7200,
});
// Refresh before expiry
const refreshed = await querri.embed.refreshSession(session.session_token);
// List active sessions
const sessions = await querri.embed.listSessions();
// Revoke a specific session
await querri.embed.revokeSession(sessionId);
// Revoke all sessions for a user
await querri.embed.revokeUserSessions(userId);

Complete Example: Multi-Tenant Embed with RLS

Section titled “Complete Example: Multi-Tenant Embed with RLS”

This example shows a complete setup where each customer sees only their data from a shared dataset.

1. One-time setup (upload data and share it):

import { Querri } from '@querri-inc/embed/server';
import { readFileSync } from 'fs';
const querri = new Querri({
apiKey: process.env.QUERRI_API_KEY,
orgId: process.env.QUERRI_ORG_ID,
});
// Upload a dataset with a tenant_id column
const file = new Blob([readFileSync('./all-tenants-data.csv')]);
const source = await querri.files.upload(file, 'tenant-data.csv');
console.log('Source ID:', source.id); // Save this
// Share org-wide (all embed users can see it, RLS filters the rows)
await fetch(`${process.env.QUERRI_HOST}/api/v1/sources/${source.id}/org-share`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.QUERRI_API_KEY}`,
'X-Tenant-ID': process.env.QUERRI_ORG_ID,
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: true }),
});

2. Per-request session creation (in your API route):

app.get('/api/querri-token', async (req, res) => {
const session = await querri.getSession({
user: {
external_id: req.user.id,
email: req.user.email,
first_name: req.user.firstName,
last_name: req.user.lastName,
},
access: {
sources: [process.env.QUERRI_SOURCE_ID],
filters: { tenant_id: req.user.tenantId },
},
});
res.json({ sessionToken: session.session_token });
});

3. Frontend embed:

<div id="querri" style="width: 100%; height: calc(100vh - 64px);"></div>
<script src="https://app.querri.com/sdk/querri-embed.js"></script>
<script>
QuerriEmbed.create('#querri', {
serverUrl: 'https://app.querri.com',
auth: {
fetchSessionToken: async () => {
const r = await fetch('/api/querri-token');
return (await r.json()).sessionToken;
},
},
startView: '/library',
chrome: { header: { show: false } },
});
</script>

Each tenant sees only their rows. No separate datasets, no per-tenant configuration. One upload, one embed, automatic filtering.


  • All communication uses postMessage with strict origin verification
  • Credentials never cross the iframe boundary directly
  • The iframe installs a fetch interceptor to attach auth headers internally
  • HTTPS is required in production
  • API keys (qk_*) should only be used server-side — never expose them in client code
  • Session tokens (es_*) are safe for the browser — they are scoped and time-limited

If your site uses a Content Security Policy, allow framing from your Querri instance:

Content-Security-Policy: frame-src https://app.querri.com;

The Querri app includes an interactive Embed UI configurator at Settings > Embed. Use it to:

  • Select authentication mode and enter credentials
  • Choose which view to embed
  • Toggle UI chrome (sidebar, header)
  • Apply custom theme colors
  • Preview the embed live
  • Copy ready-to-use embed code

The server SDK throws typed errors for different failure modes:

import { AuthenticationError, NotFoundError, RateLimitError } from '@querri-inc/embed/server';
try {
const session = await querri.getSession({ user: 'ext-123' });
} catch (err) {
if (err instanceof AuthenticationError) {
// Invalid API key
} else if (err instanceof RateLimitError) {
// Back off and retry after err.retryAfter seconds
} else if (err instanceof NotFoundError) {
// Resource not found
}
}

The SDK automatically retries on 429 and 5xx errors (up to maxRetries, default 2).

For the complete V1 API endpoint reference, see API Reference. The server SDK wraps these endpoints:

SDK ResourceAPI Endpoints
querri.users/v1/embed/users/*
querri.embed/v1/embed/sessions/*
querri.policies/v1/access/policies/*, /v1/access/resolve, /v1/access/columns
querri.files/v1/files/*
querri.projects/v1/projects/*
querri.dashboards/v1/dashboards/*
querri.data/v1/data/*
querri.sharing/v1/projects/*/shares, /v1/dashboards/*/shares
querri.sources/v1/sources/*
querri.keys/v1/keys/*
querri.audit/v1/audit/*
querri.usage/v1/usage/*