Intro
Not every customer can — or should — upgrade at the same time. Sales timelines, onboarding windows, contractual commitments and the state of the underlying data warehouse schema in a multi-tenant deployment mean your tenants might need to be on different versions of your analytics product simultaneously. A wave-based deployment strategy handles exactly this: promoting new versions to specific customers in controlled stages, while others stay pinned to what's already been validated. Here's how to implement that pattern in Omni.
01 - THE PROBLEM
The trouble with a single shared model
Omni makes it genuinely fast to build embedded analytics. You get a great semantic layer, solid embed SSO, and dynamic connection environments that handle multi-tenant data routing out of the box. The developer experience is legitimately good.
But once you're running dashboards for multiple customers in production, a familiar problem emerges: any change to your shared data model is a change for every tenant at once. Rename a field, restructure a topic, refactor a join — and you've just updated dashboards and data dependencies across your entire customer base simultaneously.
What you actually want is something closer to how a multi-tenant deployment usually works: a versioned artifact that can be validated then selectively deployed to each tenant environment (deployment stage x tenant) when the time is right. Here's how we built that on top of Omni's API.
02 - THE DESIGN
The solution hinges on a single Omni model per data model and content version, with content from a previous model version exported, re-imported and then associated to the new model with updated content specifications – all hosted within a single Omni Instance. This would result in a single model and set of associated content per active model version (as opposed to a model per tenant x environment). API-driven workflows orchestrated by CI/CD would manage artifact creation within Omni. An Operations team would manage the mapping tenant environments and model / content versions as metadata within a transactional data store.
At runtime, content paths in the Omni iFrame URL would be dynamically resolved based on a database look-up, by the parent web application, to determine which version is associated with a given (deployment stage x tenant) for a specific user session.
The versioning design
The core idea is simple: treat each Omni data model as a versioned artifact, paired with its own set of dashboards, that can be promoted independently per tenant and environment.
A few key properties the design needs to satisfy:
- → Atomic promotion — model and dashboards deploy together as a unit
- → Tenant isolation — each tenant is pinned to a specific version until explicitly migrated
- → Fully scriptable — the entire lifecycle should be automatable via Omni's API and CI/CD
- → Minimal footprint — only active versions are maintained; no accumulation of orphaned models
The good news: Omni's API surface covers almost everything you need. You'll use four areas — the Model API, the Content Migration API, Connection Environment APIs, and the Embed SSO API. Connection environment setup is a one-time prerequisite; the rest runs per deployment cycle.
Prerequisites: Before running any versioned deployments, configure your data warehouse dynamic connection environments once via POST /api/v1/connection-environments and map your environment user attribute values (tenant_a, tenant_b) to their respective data warehouse schemas. This wiring doesn't change between versions.
03 - THE DATA MODEL
The two tables you need
The versioning logic lives in your application database, not in Omni. Two tables do the heavy lifting.
dashboard_model
Maps each Omni dashboard ID to a model version number. This gets populated automatically during the import step. The dashboard_slug is a stable logical name (like patient-summary) that your app uses to look up the right dashboard ID regardless of version.
CREATE TABLE dashboard_model (
dashboard_id UUID NOT NULL, -- returned by /documents/import
model_version_id INT NOT NULL,
dashboard_slug VARCHAR NOT NULL, -- e.g. "patient-summary"
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
tenant_version
Maps each tenant + environment pair to their active model version. This is the single row your app queries at session time to know which version — and therefore which dashboards — to serve.
CREATE TABLE tenant_version (
tenant_id VARCHAR NOT NULL,
tenant_environment VARCHAR NOT NULL, -- "staging" | "production"
model_version_id INT NOT NULL,
updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (tenant_id, tenant_environment)
);
Promoting a tenant from staging to production is just an UPDATE on this table. That's it.
04 - THE WORKFLOW
The deployment workflow
Here's the full sequence for cutting Version N+1 from an existing Version N baseline. Steps 1–3 are model work; steps 4–8 are content; steps 9–10 are promotion.
| Phase | Action | Omni API Call |
|---|---|---|
| 01. Model Copy | Check out the Omni model git repo, copy the Version N directory to Version N+1 with an auto-incremented name, push to mainline. | git + CI |
| 02. Model Registration | Register the new model in Omni and capture the returned model_id for Version N+1. | POST /api/v1/models |
| 03. Model Edits | Add, update, or remove views, fields, and topics in the new model branch via YAML push. | POST /api/unstable/models/:id/yaml |
| 04. Dashboard Export | Export all dashboards associated with Version N as JSON payloads. Each response includes the full dashboard, workbook model, and any spreadsheet tile data. | GET /api/unstable/documents/:id/export |
| 05. Folder Creation | Create a new shared organization folder named v{N+1} (e.g. v2) to house all imported dashboards for this version. Capture the returned folder_id. | POST /api/v1/folders |
| 06. Dashboard Import | Re-import each dashboard, substituting baseModelId with the Version N+1 model ID and targeting the v{N+1} folder. Capture the returned dashboardId values. | POST /api/unstable/documents/import |
| 07. Mapping Update | Write a row to dashboard_model for each imported dashboard, linking dashboard_id to model_version_id = N+1 and its slug. | App DB write |
| 08. Tenant Staging | Upsert a row in tenant_version with environment = "staging" and model_version_id = N+1 for the tenant you're validating. | App DB write |
| 09. Validation | QA the staging embed session. Confirm dashboards resolve to the correct IDs and queries route to the staging Snowflake schema. | Manual / automated |
| 10. Production Promotion | Update tenant_version to environment = "production" for each approved tenant. Other tenants remain on their existing version. | App DB write |
Steps 1–8 can be fully scripted and run in a CI pipeline. Steps 9 and 10 are naturally gated — you validate in staging before promoting to production, per tenant.
What the export/import looks like in practice
The Content Migration API is the workhorse here. Export a dashboard, swap the model reference, re-import. It's less than 10 lines of curl:
# Step 4: Export from Version N
curl -L 'https://<org>.omniapp.co/api/unstable/documents/<dashboard_id>/export' \
-H 'Authorization: Bearer <TOKEN>' \
-o dashboard_export.json
# Step 5: Create the v{N+1} folder
curl -X POST 'https://<org>.omniapp.co/api/v1/folders' \
-H 'Authorization: Bearer <TOKEN>' \
-d '{"name": "v2", "scope": "organization"}'
# Step 6: Patch baseModelId and import into new folder
jq '.baseModelId = "<new_model_id>" | .folderId = "<folder_id>"' dashboard_export.json \
| curl -X POST 'https://<org>.omniapp.co/api/unstable/documents/import' \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d @-
The import response gives you the new dashboardId. Write that, the version number, and the slug to dashboard_model and you're done with this dashboard.
05 - RUNTIME
Runtime content and data resolution
At session time, your embed layer does two lookups before generating the signed URL.
Step 1: Resolve the dashboard ID
Query your two tables to get the right contentPath for this tenant and environment:
SELECT dm.dashboard_id
FROM tenant_version tv
JOIN dashboard_model dm
ON dm.model_version_id = tv.model_version_id
AND dm.dashboard_slug = 'patient-summary'
WHERE tv.tenant_id = 'acme-corp'
AND tv.tenant_environment = 'production';
Step 2: Generate the signed embed URL
Pass the resolved dashboard ID and inject environment and tenant_id as user attributes. Omni handles Snowflake routing from there — no additional application logic needed.
POST https://<org>.omniapp.co/embed/sso/generate-url
{
"contentPath": "/dashboards/<resolved_dashboard_id>",
"externalId": "acme-corp",
"name": "Acme Corp — Production",
"userAttributes": {
"environment": "production",
"tenant_id": "acme-corp",
"model_version": "2"
}
}
The environment attribute is what triggers Omni's connection environment routing — queries go to the Snowflake schema you mapped during one-time setup. For deeper per-tenant data isolation within a schema, Omni's dynamic_schemas model parameter scopes table references to a tenant-specific schema using the tenant_id attribute at query time.
06 - TRADE-OFFS
What works well and what to watch
Strengths
- √ Atomic promotion — model and dashboards deploy as a single unit
- √ Tenant isolation — other customers never see in-progress changes
- √ Fully scriptable and CI/CD-compatible
- √ All model changes enter git immediately
- √ Small artifact footprint — only active versions maintained
Watch out for
- → User-created workbooks and dashboards break when their base version is deprecated — designate a stable mainline model for user defined content
- → Content migration endpoints are currently under /api/unstable — pin your API version and monitor Omni's changelog
On API stability: The export and import endpoints (/api/unstable/documents/...) are functional and actively used in production, but the /unstable prefix means the schema can change without a major version bump. Pin the API response structure in your migration scripts and add a smoke test that catches schema drift early.
The full design — including the complete table schemas, script specs, and open architectural questions — is available as an internal technical brief. If you're working on a similar multi-tenant Omni deployment and want to compare notes, reach out.

