Skip to content

Hide bare iframe target from URL space + add SDK self-diagnostic when standalone #11

@mgoldsborough

Description

@mgoldsborough

Problem

synapseVite() in dev exposes two URL spaces with overlapping responsibilities:

Route Purpose What happens if a human opens it
/ Iframe target — internal artifact. The __preview host iframes it. Loads the SPA. Synapse handshake never fires (no parent host). Silent broken UI with empty state.
/__preview The preview host page (theme toggle, MCP bridge, iframe wrapper). Works as designed.

Vite's SPA fallback dutifully serves the bare React bundle for any unmatched path (/, /_preview, /foo, /anything). The bare bundle has no MCP bridge — useCallTool calls go nowhere — and the user sees an empty UI with no explanation. There's no signal that they're on the wrong URL.

This bit a real demo this week. The dev landed on /_preview (typo for /__preview), got served the bare bundle via SPA fallback, and saw the app's empty state asking them to "run make sync" — when in fact the data was already loaded; it was just that the UI had no way to reach the MCP server.

Workaround currently shipping in synapse-consortium

A 3-line redirect in main.tsx:

if (import.meta.env.DEV && window.top === window.self) {
  window.location.replace("/__preview");
}

This is a band-aid. It treats the symptom (bare iframe target is reachable) instead of the cause (the iframe target shouldn't share the SPA root). It also requires every app to opt in — easy to forget.

Proposed fix (two layers)

Fix 1 — Make the iframe target structurally distinct from the user-facing root

Today: /__preview iframes /.

Proposed:

Route Purpose
/ The preview host. The thing developers should land on.
/__embed (or /?_synapse=embed) Iframe target. Explicit. Cannot be reached by accident.

Now any URL a developer types — /, /_preview, /foo — falls through Vite's SPA fallback to the preview host. The bare React bundle is served only when the host page explicitly requests it via the embed route.

This is a breaking change for anyone bookmarking /__preview. Land it as a v0.5 of the plugin with a one-cycle legacyPreviewRoute: true opt-out. Apps don't need code changes either way — they just open a different URL during dev.

Implementation surface: ~30 lines in packages/synapse/src/vite/plugin.ts. Update the previewHostHtml to point its iframe at the embed route. Add a configureServer middleware that serves the bare bundle only on the embed route, and the preview host on everything else.

Fix 2 — Make the bare React app self-diagnostic at the SDK level

In packages/synapse/src/connect.ts (or wherever the handshake lives): if no ui/initialize response arrives within ~1.5s and window.top === window.self, render a small unmistakable banner over the app:

"This Synapse app is meant to be embedded. You're running it standalone — no MCP bridge is connected, so tool calls will fail. Open the dev preview at /"

Why this matters even after Fix 1:

  • Defense in depth — someone could navigate directly to the embed URL or load the bundle in a host that doesn't speak ext-apps.
  • It's a signal, not a redirect — explains why the UI is empty.
  • Production-safe: only renders if the handshake actually fails. Correct behavior in any environment.
  • ~30 lines in the SDK. Every Synapse app gets it for free.

Comparison

Approach Per-app code Bookmark survival Self-explanatory failure Long-term
Per-app main.tsx redirect (current band-aid) required n/a no
transformIndexHtml injection in plugin none yes no ⚠️ band-aid
Fix 1 (route flip) none breaks once partial
Fix 2 (SDK self-diagnostic) none yes yes
Fix 1 + 2 none breaks once yes ✅✅

Acceptance

  • Fix 1 lands in vite/plugin.ts behind a default-on flag, with a legacyPreviewRoute opt-out for one cycle. README + CHANGELOG updated.
  • Fix 2 lands in the SDK handshake path. Banner is dismissible (in case someone is intentionally testing standalone). Visual matches existing host-context theme tokens.
  • Once shipped, revert the main.tsx band-aid in synapse-apps-private/synapse-consortium and any other apps that adopted it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions