ADR-027: Superset Embedding Strategy (Phase 8.5)
Status: Accepted (2026-04-20 at Phase 8.5 ship; was Proposed since 2026-04-19) — Phase 8.5 dispatch landed all 4 workstreams in 1 Architect-session; the Architect-at-dispatch deferred-decision dispositions are documented in §"Decisions deferred to Phase 8.5 Architect — Architect-at-dispatch resolutions" below.
Date: 2026-04-19 (Proposed); 2026-04-20 (Accepted)
Supersedes: N/A (replaces W2's interim "View in Superset" anchor-link pattern at Phase 8.5 dispatch)
Related: ADR-013 (Identity Strategy — Proposed; this ADR commits PSSaaS to Keycloak via oauth2-proxy for the W2 React UI surface), ADR-020 (Shared K8s Namespaces — pss-platform is now Superset's home per Backlog #30 closure), ADR-026 (Frontend Framework + Build — Vite static build is the structural reason oauth2-proxy fits)
Context
Phase 8 W2 shipped the React operator UI at https://pssaas.staging.powerseller.com/app/ with anchor-link "View in Superset" deep-links to the 8 PowerFill dashboards (IDs 13-20 at https://bi.staging.powerseller.com/superset/dashboard/{id}/). The PO surfaced two product-instinct upgrades for the Greg-demo readiness window:
- Embedded Superset (not new-tab anchor links) — narrative coherence in the demo; one app surface, not two
- Keycloak auth on
/app/— closes the public-staging-URL security debt; satisfies the "operator must authenticate" expectation
These belong together architecturally: the embedded-SDK guest-token-mint endpoint has very different security semantics behind auth (authorization-checked passthrough) than without it (public footgun). Phase 8.5 ships them as one unit.
PSX Collaborator's reply 2026-04-19 (file paths + 8 ranked gotchas + one architectural-mismatch correction) provides the empirical primary source for the design. The reply is archived at docs-site/docs/agents/cross-project-relays/2026-04-19-psx-superset-embedding-relay.md.
Decision (Proposed)
Phase 8.5 will adopt the PSX oauth2-proxy + static-site embedding pattern (NOT the PSX NextAuth + Next.js pattern) and the PSX FastAPI 3-step guest-token-mint pattern translated to .NET 8 + HttpClient.
Architecture summary
Browser → /app/* (Vite static bundle served by nginx)
↓ (auth check)
oauth2-proxy → Keycloak (pss-platform realm)
↓ (X-Forwarded-Access-Token header)
nginx upstream → React app
↓ (fetch guest-token-mint endpoint)
/api/superset/guest-token (.NET 8; checks user role; calls Superset)
↓ (3-step server-to-server: login → CSRF → guest_token POST)
bi.staging.powerseller.com /api/v1/security/guest_token/
↓ (returns scoped guest token)
React app → @superset-ui/embedded-sdk embedDashboard()
↓ (MessageChannel handshake; renders iframe)
bi.staging.powerseller.com /embedded/{uuid}/ (auth-gated by guest token)
Key architectural decisions
(D-8.5-1) PSX's oauth2-proxy pattern is the right reference, NOT PSX's NextAuth pattern. Per PSX Collaborator's Q3 caveat: the W2 React UI is a Vite-built static bundle served by nginx — structurally identical to PSX's Docs / Grafana / Superset-admin static-site surface, and structurally distinct from PSX's Next.js app. Translating NextAuth to a Vite static bundle is non-trivial because there's no server runtime to do the OIDC code exchange. oauth2-proxy is the correct shape for our build pattern. Reference files in PSX repo: infra/oauth2-proxy/oauth2-proxy.cfg and infra/azure/scripts/configure-keycloak.ps1 (search for docs-proxy — the canonical static-site Keycloak client).
(D-8.5-2) Guest-token-mint endpoint lives in the .NET 8 PSSaaS API. Same shape as PSX's FastAPI endpoint at api/routes/principal.py:242-313. The .NET 8 implementation translates the 3-step Superset handshake (login → CSRF → guest_token POST) to C# + HttpClient. The CSRF step is the most-missed piece — Superset's guest_token endpoint requires X-CSRFToken header even though it's an API endpoint. Auth check happens at the .NET endpoint via the X-Forwarded-Access-Token header forwarded by oauth2-proxy.
(D-8.5-3) Per-resource scoping: PSSaaS will start with one-token-per-dashboard (Option A from PSX), then evaluate one-token-covering-multiple-resources (Option B) if 8 separate token mints become a UX or performance issue. PSX picked Option A for simplicity; PSSaaS has 8 dashboards instead of 1, so the calculation may differ. Decision deferred to Phase 8.5 Architect with this ADR's Option B as documented escape hatch.
(D-8.5-4) Per-dashboard registration: PSSaaS will write the script PSX explicitly does not have. PSX registers each embed-enabled dashboard manually via Superset admin UI; PSSaaS has 8 dashboards, so manual click-through is friction-prone. Phase 8.5 ships an idempotent registration script using Superset's /api/v1/dashboard/{id}/embedded/ endpoint (POST to create; PUT to update). The captured UUIDs persist to a config map (likely infra/superset/powerfill-embed-uuids.json or similar) for the .NET API to read.
(D-8.5-5) Required Superset config changes are platform-Superset-side, owned by PSX Infra. Per PSX Collaborator's Q2 (lines 81-95 of infra/superset/superset_config.py):
HTTP_HEADERS = {"X-Frame-Options": "ALLOWALL"}+TALISMAN_ENABLED = False— without these, the iframe never rendersFEATURE_FLAGS["EMBEDDED_SUPERSET"] = True— embedded API endpoints don't exist without itGUEST_TOKEN_JWT_SECRET— production override required; same secret across all instances of Superset that share a database (per gotcha #7)PUBLIC_ROLE_LIKE = "Gamma"— applies at fresh DB init only (gotcha #1; see "Risks" below)
PSX's platform-Superset already has EMBEDDED_SUPERSET on (PSX uses it for their Principal dashboard). PSSaaS verifies + amends as needed via PSX Infra collaboration.
Decisions deferred to Phase 8.5 Architect — Architect-at-dispatch resolutions (2026-04-20)
- Specific .NET 8 HttpClient implementation shape — RESOLVED: typed client via
services.AddHttpClient<SupersetGuestTokenClient>(...)inModules.Superset/SupersetExtensions.cs. No Polly retry in v1 (acceptable per the empirical-failure-bubble-up-to-embed-side-error pattern in W2's EmbeddedDashboard.tsx; banked as "implementation-shape decision deferred to dispatch time" pre-plan; v1 ships without retry to keep failure modes surfacing cleanly through the embed-side error block per A69 honesty pattern). CSRF token caching is implemented via process-localSemaphoreSlim-guarded session memo withLoginSessionCacheSeconds = 300default, refreshed-on-401 (handles mid-session admin credential rotation cleanly). - Endpoint shape:
/api/superset/guest-tokenvs/api/powerfill/guest-tokenvs per-dashboard endpoints — RESOLVED via Alternatives-First Gate (Phase 8.5 plan §3 (a)): chose cross-cutting/api/superset/guest-tokenwith body{ "dashboard_key": "hub|guide|recap|switching|poolCandidates|existingDisposition|poolingGuide|cashTradeSlotting" }. Phase 10+ surfaces (Pipeline UI, Risk UI) inherit the same endpoint shape; only thedashboard_keyset extends. The endpoint lives in a newModules.Supersetmodule per ADR-004 modular-monolith pattern. Per-dashboard endpoints (chatty; 9 endpoints) and per-module endpoints (forces parallel/api/risk/guest-tokenetc. duplication at Phase 10+) both rejected. - React component shape — RESOLVED:
<EmbeddedDashboard>shared component atsrc/frontend/src/components/EmbeddedDashboard.tsx(mirrors PSXweb/app/principal/components/SupersetEmbed.tsxshape). Dynamic-import of@superset-ui/embedded-sdkso it lazy-loads (matches PSX pattern; verified via Vite build chunk separation atlib-Cb7LCDYX.js). iframe-sizing trick is the PSX containerRef-polling approach (poll 100ms untilcontainerRef.current.querySelector('iframe')returns non-null, then apply width/height/border-radius styles). Cleanup contract: AbortController on initial fetchGuestToken + clearInterval on poll + embedded.unmount() on unmount, mirroring the AGENTS.md async-leak countermeasure shape from useApi.ts / useReportFetch.ts. - Anchor-link replacement strategy: full-replace atomic vs feature-flag per-page migrate — RESOLVED via Alternatives-First Gate (Phase 8.5 plan §3 (c)): chose full-replace atomically. The header "View in Superset" anchor link in
reportShell.tsxlines 103-113 became a smalldata-test-id="superset-embed-marker"indicator + the embedded iframe rendered ABOVE the GenericTable. The Hub-anchor inHome.tsxlines 42-54 + the per-report Superset anchors inRunStatus.tsxlines 190-198 + the Hub-anchor inRunStatus.tsxlines 213-220 all replaced with inline<EmbeddedDashboard>instances. A66 BLUE banner UX preserved above embeds (FreshnessBanner renders BEFORE the embed; forTerminalEmptyverdict, the embed is intentionally NOT rendered to avoid confusing-empty-iframe). Feature-flag rejected per ADR-027 + kickoff non-negotiable; embed-fail-fallback rejected per A66/A69 honesty pattern. - Per-resource scoping (Option A vs Option B) — RESOLVED: stayed on Option A (one token per dashboard) for v1 per Phase 8.5 plan §3 (b). Matches PSX reference exactly; minimal blast radius if a token leaks. 9-mint-per-session chattiness flagged for Phase 10+ revisit if it surfaces as a UX issue.
Consequences
Positive
- Demo narrative coherence. Greg sees one app, not "PSSaaS that links out to Superset."
- Branding/credibility. Embedded dashboards strip Superset's chrome — looks like PSSaaS analytics, not "third-party tool we forwarded to."
- Filter pre-population. Embedded dashboards take URL state at mount time; the React UI can pre-set "show me the run I just ran" or "filter to this loan" — much harder to coordinate via new-tab navigation.
- Auth lands. Public-staging-URL security debt closed; A68 long-term decoupling (TenantId from Keycloak claim) gets natural fold-in.
- Pattern composability. Future PSSaaS surfaces (Pipeline Management UI, Risk Manager UI, etc.) inherit the same auth + embed shape.
Negative
- Phase 8.5 is 2-3 Architect-sessions of focused work. Faster than the original "auth-deferred-to-Phase-9+" path would have been; slower than "ship anchor links and call it done."
- Coupling to platform-Superset configuration. PSSaaS now depends on
EMBEDDED_SUPERSETfeature flag,GUEST_TOKEN_JWT_SECRET,PUBLIC_ROLE_LIKE,TALISMAN_ENABLED=False,HTTP_HEADERS=ALLOWALLall being correctly set on platform-Superset. Mitigated by PSX Infra ownership + PSX Collab having already debugged these for their Principal dashboard. - Per-dashboard UUID lifecycle. If a dashboard is re-created (vs updated), the UUID changes. Need to verify our
deploy-powerfill.pyidempotent re-run preserves UUIDs, OR document the re-registration step. - 24-hour-rollback-window from Backlog #30 is gone — by Phase 8.5 dispatch, the old
psx-stagingSuperset deployment will have been fully decommissioned. Rollback to the old Superset is no longer possible. Not a Phase 8.5 risk; flagged for completeness.
Operational
- PSSaaS oauth2-proxy Deployment + Service in
pssaas-stagingnamespace, fronting/app/and/api/paths. Replaces W2's current direct ingress-to-frontend / direct-ingress-to-API pattern. - New Keycloak client in
pss-platformrealm — call itpssaas-app(analog of PSX'sdocs-proxy). Confidential client; Vault-managed secret; standard flow only. - New ingress rules that delegate
/app/and/api/paths to oauth2-proxy upstream first, with oauth2-proxy forwarding to the existingfrontendandapiservices after auth. - GitHub Actions deploy workflow extends to deploy oauth2-proxy alongside the existing api/docs/frontend pipeline.
- Cross-cutting: A68 (tenant-id-vs-config-slot conflation) gets natural closure when Keycloak's
tenant_idclaim becomes the canonical TenantId source.
Risks (PSX Collaborator's gotchas, ranked by cost)
- PUBLIC_ROLE_LIKE silent no-op on existing Superset (PSX gotcha #1). On platform-Superset (existing DB, not fresh init), this config does nothing. Embedded dashboards fail with cryptic "embedded authentication" errors that don't obviously point to "Public role is empty." Mitigation: at Phase 8.5 dispatch, run
copy_gamma_to_public.pyscript OR manually grant Public role Gamma's permissions via Settings → Security → List Roles → Public. PSX Infra collaboration required. - TALISMAN_ENABLED + X-Frame-Options nginx override (gotchas #2-3). Superset's default CSP blocks all framing; nginx-side X-Frame-Options can override Superset's permissive setting and re-block. Mitigation: explicit X-Frame-Options unset in PSSaaS nginx + verify TALISMAN_ENABLED=False on platform-Superset.
- CORS on guest-token endpoint (gotcha #4). Avoided by our pattern (React fetches token from same-origin .NET API; .NET fetches from Superset server-to-server). Flagged so future PSSaaS surfaces don't accidentally cross origins.
- Superset 3+ MessageChannel-only (gotcha #5). URL-param guest token delivery is dead in Superset 3+. Old StackOverflow answers showing
?guest_token={token}won't work. Mitigation: SDK 0.3.0+ handles MessageChannel; we just use the SDK. can_log on Supersetv6 permission (gotcha #6). Same family as #1 (Public role missing perms). Mitigation: include in the Public-role-permission audit.- GUEST_TOKEN_JWT_SECRET coordination across instances (gotcha #7). Rotation invalidates existing tokens mid-session. Mitigation: PSX Infra handles secret rotation; PSSaaS doesn't rotate independently.
- UUID ≠ dashboard ID (gotcha #8). UUID generated when embedding is enabled per dashboard; not the integer ID we already know. Mitigation: capture UUIDs at registration time; persist to config map; .NET API reads from there.
Decision provenance
- PO direction 2026-04-19: "I'd like to do it in staging, and I should have to authenticate via Keycloak to access /app" + product-instinct on embedded-vs-anchor-links
- PSX Collaborator reply 2026-04-19 (archived at
docs-site/docs/agents/cross-project-relays/2026-04-19-psx-superset-embedding-relay.md): authoritative file-path references + the load-bearing Q3 architectural-mismatch correction (oauth2-proxy + static-site, not NextAuth + Next.js) + 8 ranked gotchas - Backlog #30 closure (commit
f920668): platform-Superset operational; embedding can target the stablebi.staging.powerseller.comhostname
Architect-at-dispatch checklist
When Phase 8.5 is dispatched (post-Phase-9-completion per PO sequence preference; Phase 9 currently in flight), the Architect should:
- Re-read this ADR + the cross-project-relays archive entry
- Verify the
pssaas-appKeycloak client doesn't already exist; if not, create it (or coordinate with PSX Infra to create it) - Verify platform-Superset's
EMBEDDED_SUPERSETflag,PUBLIC_ROLE_LIKEactual-state-on-existing-DB, andTALISMAN_ENABLEDsetting via PSX Infra collaboration - Decide the deferred items (D-8.5 deferred list above) per Alternatives-First Gate
- Implement the 8-dashboard registration script as the first deliverable (lowest-risk; isolatable; lets the per-page React work proceed in parallel)
- The PSSaaS oauth2-proxy + nginx config + Keycloak client setup are the load-bearing infrastructure pieces; the React component changes are lighter follow-on