Embed a Rig data app in Salesforce, under each user's own permissions
Put a live Rig dashboard on a Salesforce tab or record page. Salesforce proves who is looking, Rig resolves that person to their own Rig account, and the app renders under their permissions — not the author's. Buttons in it can write back to Salesforce as that same person. Someone with no Rig account sees nothing at all.
What you get
A Rig data app is an HTML dashboard built on your warehouse. Embedding it in Salesforce means your reps never leave the CRM to see it — and, unlike a share link, the embed carries an identity.
The flow is a Salesforce Canvas app. When a rep opens the tab, Salesforce POSTs a signed request to Rig containing that user's email, signed with your Connected App's consumer secret. Rig verifies the signature, checks the request came from the one Salesforce org your workspace is paired with, resolves the email to a Rig account, and issues a 15-minute token scoped to that person and that app. The dashboard then re-runs for them, so schema permissions, row filters and column masks all apply per viewer.
Two consequences worth being clear about up front. A rep with no Rig account gets a card reading No Rig account for jane@acme.com and no data — that is the intended behaviour, not a misconfiguration. And the record scope is signed into the token, so a rep cannot edit the URL to point the app at an account they aren't looking at.
Set it up
One Connected App covers both halves: framing Rig in Lightning, and giving Rig an API credential to read or write Salesforce records. If you have already connected Salesforce as a data source, you have most of this — see Connect Salesforce for the credential half and Salesforce access for migration & audit for the least-privilege variant.
In your Salesforce org, an admin does this once:
- Setup → App Manager → New Connected App. Name it Rig Data Apps and set a contact email.
- Enable OAuth Settings. Callback URL https://app.rig.so/oauth/callback (required by the form; unused by this flow). Scopes: api and refresh_token, offline_access.
- Configure Canvas. Canvas App URL https://app.rig.so/{your-workspace}/embed/session — we give you the exact value. Access Method Signed Request (POST). Locations: add Visualforce to drop it on a record page or tab, Chatter for a standalone tab.
- Edit Policies → Permitted Users: Admin approved users are pre-authorized, then assign the profiles or permission sets that should see the app. Salesforce only signs a request for users who have access to the Connected App, so this list is your "who can open it in Salesforce" control.
- Setup → Security → CSP Trusted Sites: add https://app.rig.so, context frame-src. Without it Salesforce's own policy refuses to frame us and you get a blank panel with nothing in our logs.
- Send Rig, over a secret channel (1Password, not Slack or email): your My Domain URL, your Organization ID, and the Connected App's Consumer Key and Consumer Secret. We pair the Org ID to your workspace, so a signed request from any other Salesforce org is refused.
Then surface it: a Canvas App Tab, or a Visualforce page carrying <apex:canvasApp developerName="Rig_Data_Apps" /> that you drop onto a record page. To scope the app to the record it sits on, pass the id on the Canvas App URL query string — ?account_id={!Account.Id} — where the parameter name matches an input the data app declares.
Pinning one specific app is ?app=<data-app-id>; without it the workspace's default embedded app renders.
For several tabs, don't put the app id on the URL. The Canvas App URL belongs to the Connected App, so every tab shares it — and a ?app= there wins over anything a page passes, which quietly makes all your tabs show the same dashboard. Leave it off and let each page name its own: <apex:canvasApp developerName="Rig_Data_Apps" parameters="{'app':'<id>'}" />. Salesforce signs those parameters into the request, so one Connected App backs as many pages as you like.
Building the app with Claude
The dashboard itself is a normal Rig data app, so you build it the way you build any other — in chat, or with Claude connected to your workspace over MCP. See Vibe-code your dashboards.
One constraint matters for embedding, and it is worth putting in your prompt. An app is re-rendered for each viewer only if it is deterministic: SQL templates feeding an HTML template. An app whose output depends on an LLM at render time can't be re-run per person, so Rig falls back to the stored run — and if the viewer's role carries row filters or column masks, it refuses to show it rather than hand them the author's unfiltered output.
So ask for the deterministic shape:
"Build a data app for the Salesforce embed: one trigger node and one skill node, output_format html, SQL templates for the numbers and a static HTML template — no LLM at render time, so it re-renders per viewer and row filters apply. Declare an account_id trigger input so it can be scoped to the record it sits on."
Ask Claude to confirm the app re-renders per viewer before you wire it to a tab. If it reports the app falls back to a stored run, the fix is to move the narrative work out of the render path.
Who sees what
This is the part people get wrong, because "it's inside Salesforce" suggests Salesforce is enforcing the permissions. It isn't — it depends entirely on where the data comes from.
Warehouse data, read through Rig
Rig's RBAC is the only authority. Your warehouse has no idea Salesforce exists, and a Salesforce profile grants nothing in it. What applies is the viewer's Rig role: which schemas and tables they can read, any row filters on those tables, and any column masks or sensitivity policies. All of it is evaluated per request, and the role is re-read from the database at render time — so revoking someone's access takes effect within seconds, not when their token expires.
Set this up once in role-based access control. The embed inherits it; there is no second set of permissions to maintain.
Salesforce objects, read live through Rig
Salesforce field-level security and sharing rules do not apply here. When a data app reads or writes Salesforce records through Rig, it authenticates with your Connected App's own credential and runs as that app's Run As user. That user's profile is the ceiling on everything Rig can see or change, and a write is attributed to it — not to the rep who clicked. Rig's own role checks are the only per-person gate on that path.
The lever you have is the Run As user's profile: make it a dedicated integration user and grant it exactly the objects and fields Rig should touch, and nothing more. Where a write is destructive or field-sensitive, do it host-side — the embed can post the action up to the page that frames it, and your Apex or LWC performs it as the logged-in rep, at which point Salesforce's sharing rules and FLS do apply and the CRM audit trail names the real person.
Reads of the synced Salesforce data in your warehouse are the first case, not this one — they are warehouse tables and Rig's RBAC governs them.
When the email doesn't match
The join key is email: Salesforce asserts one, Rig looks for an account with the same address in your workspace.
Salesforce asserts the User's Email field, not the Username. That distinction resolves most of the worry. Usernames must be unique across all of Salesforce, so orgs suffix them into things like jane.smith@acme.com.sandbox or tm.e06028a475bc@agentforce.com — while the Email field on the same record is the person's real address. Check the Email column in Setup → Users before assuming you have a mapping problem.
Genuine mismatches do happen — someone's Salesforce record says j.smith@acme.com and their Rig login is jane@acme.com, or the two systems sit on different domains after an acquisition. There is no mapping table in Rig today. Two options:
- Align the addresses — usually easiest, since the Salesforce Email field is editable and is what the assertion carries.
- Email product@rig.so with the pairs that don't line up. We'll sort it for your workspace and it's on the roadmap to make self-serve.
Per-user rows: comp and quota
The common case for an embedded app is one every rep can open but where each sees only their own numbers — commission, quota attainment, their book. You don't build a dashboard per rep, and you don't filter in the SQL. You designate the table as user-scoped and Rig injects the filter.
In the admin access settings, under User-scoped tables, add the table and name the column that identifies the person — a rep email column on your comp table, say. From then on, every query against that table, from any surface, gets WHERE rep_email = <the viewer> injected at rewrite time. Matching is on login email, or on an account name each user sets in their preferences when your warehouse stores names rather than addresses. Case is folded on both sides, which matters because CRM-sourced email columns routinely store mixed case.
Owners and admins are exempt and see every row — that is what makes the same app work for a rep and for their VP. It also means you can't check the filter by looking at it yourself as an owner; use a role that isn't exempt, or Rig's "view as" preview.
An app that reads a user-scoped table also becomes personalized: its runs are private to the person who triggered them, it can't be publicly shared or scheduled, and each viewer auto-runs it to get their own rows. That is exactly the behaviour you want behind a comp dashboard, and it is enforced rather than conventional.
Writing back to Salesforce
An embedded app isn't read-only. A button in it can update a Salesforce record — and the update runs as the person who clicked, so their sharing rules and field-level security apply and the audit trail names them rather than a service account.
The app calls one line. Rig relays it up to the Salesforce page hosting the embed, and your Apex performs the change:
await window.RigDataApp.action("host.update-opportunity", {
opportunity_id: "0065g00000XyZaBAAV",
fields: { Amount: 48000 },
});You install a small Apex adapter in your org once. It decides what is allowed: an explicit list of permitted actions, an explicit list of writable fields, and a per-field check that the viewing user could have made that edit by hand. Rig sends an intent; your org decides whether to honour it. Ask us for the adapter — it's about 100 lines and yours to review.
There is a second route, where Rig performs the write itself through your Salesforce connection. It's the right choice for something a rep shouldn't be able to do by hand, but it runs as your integration user and the audit trail names that user. For anything a rep would normally do themselves, prefer the host-side route above.
Choosing the right shape
Three questions decide the architecture, and they're much cheaper to answer before you create a Connected App than after.
- Does everyone see the same numbers? If yes, you don't need any of this — a normal Rig share link in a Salesforce Web Tab is enough. The per-user embed exists for when reps should see different data.
- Where does the data live? Warehouse data is governed by Rig's permissions. Live Salesforce records are governed by Salesforce's — so read those natively in the page rather than through Rig, and you get sharing rules for free. Salesforce data you want to analyse over time is better synced into your warehouse.
- Does anything write, and must it be attributed to the rep? That's the difference between the two routes above.
The strongest shape available today puts both halves on one page: native Salesforce panels reading records under the viewer's Salesforce permissions, and a Rig panel reading the warehouse under their Rig role. Each system enforces the data it owns, and neither has to trust the other.
Troubleshooting
- Blank panel, nothing in Rig's logs. Salesforce's own CSP is refusing to frame us. Add https://app.rig.so to CSP Trusted Sites with context frame-src.
- "No Rig account for …" The identity arrived fine; that person just isn't in your Rig workspace, or their Rig address differs. See when the email doesn't match.
- "… has no access to this workspace" They have a Rig account but no role in this workspace. An admin grants one.
- "This CRM org is not linked to this workspace" The assertion is valid but came from a different Salesforce org than the one paired to your workspace. Send us the Org ID you're actually embedding from.
- The tab never renders for anyone. Check Permitted Users on the Connected App — Salesforce only signs a request for users whose profile or permission set is assigned to it, and a new Connected App takes a few minutes to propagate.
- The tab isn't in the App Launcher at all. Custom tabs are created hidden on every profile, with no warning — the tab exists and simply doesn't appear. Setup → Profiles → your profile → Tab Settings, set it to Default On.
- The dashboard is blank inside Salesforce but fine on its own. Heavy dashboards — large chart libraries, very large embedded datasets — can fail to paint inside the Canvas frame. Try a lighter version of the app to confirm, then trim what it loads at render time.
- A rep sees fewer rows than expected. That is usually the feature working. Check their Rig role for row filters, and whether the table is designated user-scoped.
Still stuck? Email product@rig.so with your workspace and the Salesforce Org ID and we'll read the session logs with you.