Merchant-facing embeds and links
How to embed shop-locked dashboards and create stable links without exposing Metabase filters.
Merchant-facing embeds and links
Merchants never need a Metabase account. The data plane locks each dashboard to a shop UUID, mints a short-lived Metabase token, and sends the browser to that shop-specific view. Shop names never belong in embed URLs or query parameters.
Choose the right link
| Use case | URL | Authentication | Lifetime |
|---|---|---|---|
| Product iframe | GET /v1/embed/reports/shop_sales?shopId=<uuid> |
Data-plane bearer token with shop access | Returned Metabase URL lasts 10 minutes; refetch it |
| Internal share or bookmark | GET /e/shop_sales/<uuid> |
Valid session/bearer with shop access at click time | The Joe URL never expires |
| Merchant email, Slack, or iframe without a session | GET /e/shop_sales/<uuid>?exp=<unix>&sig=<hmac> |
Signature in the URL | 7, 30, or 90 days; 90 days by default |
Both /e/ forms mint a fresh 10-minute Metabase token when clicked and respond with a 302 redirect. A bookmarked /e/ URL therefore does not inherit the Metabase token’s expiry.
Copy-paste examples
Use canonical shop UUIDs only:
https://data-plane.joe.coffee/e/shop_sales/4a2729c3-5be1-4d1e-9b0d-985ac242f0e3
The equivalent query form is supported for clients that cannot put the shop in the path:
https://data-plane.joe.coffee/e/shop_sales?shop=4a2729c3-5be1-4d1e-9b0d-985ac242f0e3
Do not hand-write sig values. Joe staff should open the reports admin, search for the shop by name, choose the report and 7/30/90-day expiry, then copy the generated no-login link. The page also shows the plain authenticated link for internal sharing.
For an application iframe, request a fresh URL from the authenticated API:
GET /v1/embed/reports/shop_sales?shopId=4a2729c3-5be1-4d1e-9b0d-985ac242f0e3
Authorization: Bearer <short-lived-shop-scoped-token>
{
"ok": true,
"data": {
"iframeUrl": "https://joe-metabase.fly.dev/embed/dashboard/<short-lived-token>#bordered=false&titled=false",
"expiresAt": "2026-07-20T21:10:00.000Z"
},
"meta": {}
}
Set iframe.src to data.iframeUrl and refetch before expiresAt. Never append a shop name, shop_id, or a replacement filter to the Metabase URL.
Security model
- The authenticated path calls the same shop-access assertion as the embed API. A control-plane token can only open a UUID listed in its
shopIdsclaim. - No-login links use HMAC-SHA256 with the server-only Metabase embed secret and a domain-separated
link:v1:payload. The signature covers report key, shop UUID, and optional Unix expiry. - The Worker checks the report registry and the
shopstable before minting. Unknown reports, unknown shops, invalid signatures, and expired signatures receive a generic branded404or403page without configuration details. - The URL signature authorizes the stable Joe link, not Metabase directly. The Metabase JWT remains short-lived and keeps
shop_idlocked. - Treat a no-login URL like a password until it expires. Forwarding it forwards its access. Generate a shorter replacement when that is the safer choice.
Scheduled merchant-report emails use 90-day signed /e/ links. Internal report emails continue to link directly to authenticated Metabase dashboards.
Adding an embeddable report
- Build the dashboard in Metabase with a mandatory
shop_idfilter and merchant-safe labels. - Enable static embedding and mark
shop_idas a locked parameter. - Add the report key, human label, dashboard binding, and locked-param function to
src/embed/report-registry.ts. - Add the optional dashboard ID binding to
src/env.tsand document deployment configuration. - Verify the authenticated API, both
/e/forms, the admin dropdown, and generic failure pages.
See Merchant Manager integration for an application migration recipe.