Skip to content

experiment: render the real PostHog webapp in-app via local auth proxy#3475

Draft
mariusandra wants to merge 3 commits into
notebooks-in-codefrom
experiment/embedded-app
Draft

experiment: render the real PostHog webapp in-app via local auth proxy#3475
mariusandra wants to merge 3 commits into
notebooks-in-codefrom
experiment/embedded-app

Conversation

@mariusandra

@mariusandra mariusandra commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Experiment 2/3: <PostHogApp url="/notebooks" />

Part of the notebook node-editing parity experiments (stacked on #3450). Companion posthog PR: PostHog/posthog#71087. Verdict from the run: keep / iterate.

Renders the real PostHog webapp (React 18 + kea) inside the code app (React 19), isolated in an iframe with a postMessage bridge. Verified live in the desktop app: the notebooks scene with a live-querying embedded insight, the feature-flags scene, and a heavy production dashboard computing trends/funnels — all with host-synced light/dark theme and bidirectional navigation.

Architecture

  • EmbeddedAppProxyService (workspace-server, main process, modeled on AuthProxyService): serves an HTML shell booting the webapp's new embed vite entry, and stream-proxies /api/*, /_preflight, media/static to the region cloud host, injecting a fresh OAuth Bearer via AuthService.authenticatedFetch. The iframe sees a plain same-origin API — no CORS, token never enters any renderer.
  • embeddedApp.getUrl host-router procedure lazy-starts the proxy on an OS-assigned port.
  • PostHogApp component in packages/ui (route /embedded-app): iframe + bridge client — host→iframe navigate/setTheme, iframe→host ready/routeChanged/openTab; theme re-synced on every ready (the webapp hard-reloads its document on some scene transitions).

Chrome-less mode + browser-tab integration (round 2)

  • The webapp's own nav chrome (project sidebar, top bar, side panel) is hidden in embed mode — the upstream navigationLogic gained an embedMode reducer that short-circuits its mode selector to 'none' (the existing tested no-chrome path), so the scene fills the iframe and the code app provides the surrounding chrome.
  • Embedded surfaces are first-class browser tabs: the webapp path is encoded into the tab's appView string ("embedded-app:<path>"), giving per-path tab identity, dedup, and restart persistence with zero schema change. Tabs get a globe icon + "PostHog " label.
  • window.open / target="_blank" inside the embed bridges out as openTab and spawns a real host tab (verified live: two PostHog tabs coexisting, switching, and surviving app restart); external URLs keep native behavior (system browser). A "+ New tab" button on the embedded-app toolbar opens the current page as a new tab (identity-deduped, browser-like).

Broken-endpoint catalogue (the key finding)

One failure class: DRF actions without required_scopes reject token auth with 403 — the typed query subresources (POST /query/TrendsQuery/ etc.) break Activity, Persons, and fresh insight creation, plus tags and user_product_list. Classic /query/ works (which is why dashboards render). Small, enumerable upstream fix. (These are the orange tiles visible in the screenshots.)

Dragons

  • Proxy binds 127.0.0.1 with no per-session auth secret yet — any local process could use it while running. Must fix before shipping.
  • Tab switches remount the iframe (single-Outlet, no keep-alive) — each switch reloads the webapp; a persistent portaled iframe host would be the fix if this graduates.
  • Production packaging (build the embed entry into dist, serve manifest-resolved assets from the proxy, decide webapp update cadence) designed but not built.
  • posthog-js tracking deliberately dead in embed mode; replay playback, websockets, uploads untested.

Screenshots

Chrome-less embed, dark theme — no webapp sidebar, scene edge-to-edge, "PostHog /notebooks" as a browser tab, "+ New tab" in the toolbar (orange tiles are the documented required_scopes 403s):

Embedded webapp, dark, chrome-less

Same notebook, host + embed in light theme, two PostHog tabs in the strip (restart persistence visible):

Embedded webapp, light, two tabs

Result of window.open inside the embed — a second real host tab:

window.open spawns a host tab

🤖 Generated with Claude Code

mariusandra and others added 2 commits July 15, 2026 15:37
…uth proxy

EXPERIMENT: <PostHogApp url="/notebooks" /> renders the cloud webapp inside
an iframe, isolated from the React 19 host tree.

- EmbeddedAppProxyService (workspace-server, bound in main like AuthProxy):
  serves an HTML shell booting the webapp's `embed` vite entry, and proxies
  /api, /_preflight, /uploaded_media, /static to PostHog cloud with a fresh
  OAuth Bearer token from AuthService — the iframe sees a same-origin API,
  no CORS, no credentials in the renderer.
- host-router embeddedApp.getUrl starts the proxy lazily and returns its URL.
- packages/ui PostHogApp component + /embedded-app route: iframe plus
  postMessage bridge (navigate, setTheme, ready, routeChanged) synced to the
  host theme store.
- bootstrap: POSTHOG_CODE_APP_NAME env (dev only) isolates userData and the
  single-instance lock so a second dev instance can run beside another
  checkout's app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An iframe reload sends ready again but the ready state is already true, so
the theme effect wouldn't re-run and the embed could boot with a stale theme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

React Doctor found 2 issues in 1 file · 2 warnings.

2 warnings

src/features/embedded-app/EmbeddedAppView.tsx

Reviewed by React Doctor for commit 53b7878.

The embedded webapp is now a first-class browser tab:

- /embedded-app takes a `path` search param; each distinct webapp path is
  its own tab identity, encoded into the tab's appView string
  ("embedded-app:<path>") — the browser-tabs domain treats appView as an
  opaque string, so dedup/focus works per path with no schema change.
- useAppView maps the route (with its search path), BrowserTabStrip labels
  embedded tabs "PostHog <path>" with a globe icon and navigates them back
  to /embedded-app?path=... on select.
- useOpenEmbeddedAppTab opens (or focuses) a NEW tab: it mirrors the
  strip's open flow — openOrFocus transform applied local-first, persisted
  in the background, then a navigation stamped with the tab id — because a
  plain router navigation is in-tab by design and would retarget the
  current tab.
- PostHogApp handles the bridge's `openTab` messages (window.open /
  target=_blank inside the webapp) by opening a host tab at the normalized
  path (/project/<id> prefix stripped so identities stay stable), and grows
  a "+ New tab" toolbar affordance for the current embedded page.

Verified live: nav chrome hidden (posthog-side embedMode), scene
full-width, window.open in the embed spawns a second "PostHog /notebooks/…"
tab, tabs persist across app restart and switch correctly, theme and
navigation bridges intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant