EMBEDDABLE BRIDGE
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Embeddable Bridge Architecture
How GoGrab AI is embedded across different host applications, Chrome extensions, and standalone contexts.
Library: @libs-ux/embeddable-bridge
Path: libs/libs-ux/embeddable-bridge/
Last updated: Session 105.3 (February 2026)
Table of Contents
- Overview
- Architecture Diagram
- Three Embedding Approaches
- URL Parameters Reference
- Workspace Port Derivation
- Bridge Event Model
- Authentication Flows
- Component Reference
- Comparison Table
- Troubleshooting
Overview
The Embeddable Bridge library enables the gograb-ai Angular application to be embedded inside other applications as an iframe with structured, type-safe communication. It provides:
- Iframe hosting with sandboxing, loading/error states, and auto-reconnection
- PostMessage bridge with origin validation, message typing, and heartbeat monitoring
- Workspace-aware port derivation so the same code works across
editor2(5xxx),gspace1(6xxx), and future workspace clones - Authentication forwarding via both URL parameters and bridge messages
- Drag-and-drop support from the embedded AI panel to the host canvas
The gograb-ai application serves two main routes for embedding:
| Route | Purpose |
|---|---|
/panel |
AI Creation Studio -- chat, image, video, widget, module generation |
/observatory |
Agent Observatory -- real-time monitoring of Vitto and Orchestration agents |
Architecture Diagram
+---------------------------+ +---------------------------+
| HOST APPLICATION | | gograb-ai App |
| (gograb-space, admin, | | (separate Angular app) |
| dev, or extension) | | |
| | | Routes: |
| +---------------------+ | | /panel -> Studio |
| | GoGrabAiPanelComponent|----+ | /observatory -> Monitor |
| | or | | | | |
| | GoGrabObservatoryComp| | | | +---------------------+ |
| +---------------------+ | | | | EmbeddedAdapterSvc | |
| | | | | | - Listens for msgs | |
| +---------------------+ | | | | - Sends replies | |
| | EmbeddableHostComp | | | | | - URL param auth | |
| | - Creates <iframe> | | | | +---------------------+ |
| | - HostAdapterService | | | +---------------------------+
| | - Origin validation |-----|--> iframe (sandboxed)
| | - Heartbeat monitor | | | src="http://localhost:
| +---------------------+ | | 6400/panel?embedded=true
| | | &theme=dark&..."
+---------------------------+ |
|
PostMessage Bridge <-------+
(bidirectional, typed)
Three Embedding Approaches
1. Direct Component (Native)
Used when the AI panel lives in the same Angular application as the host.
- Where:
gograb-spacemain layout - Component:
<lib-ai-panel>(from@libs-ai/vitto-hub) - Communication: Direct Angular inputs/outputs, shared DI tree
- Auth: Shared Angular auth service (same app context)
- No iframe, no bridge, no port concerns
Since Session 105.3, the native panel uses <ai-panel-tabs> which wraps both <lib-ai-panel> (Studio) and <agent-observatory> (Observatory) with a tab switcher.
2. Embeddable Bridge (Cross-App Iframe)
Used when embedding the AI panel from a different Angular application via iframe.
- Where:
gograb-admin,gograb-dev, or any Angular host - Components:
<gograb-ai-panel>or<gograb-observatory> - Communication: PostMessage via
HostAdapterService<->EmbeddedAdapterService - Auth: Bridge auth (host sends
AI_AUTH_INITafter iframe is ready) - Port: Auto-derived from host port via
deriveAiPort()
3. Chrome Extension Iframe
Used when embedding inside the Chrome extension's side panel.
- Where:
apps/gograb-ai-extension/sidepanel/ - Communication: Vanilla JS
ExtensionBridgeAdapter(implements same protocol) - Auth: URL parameters (
token,userId,userEmail) -- primary path; bridge auth as fallback - Port: Runtime probing across known ports (5400, 6400, 4200)
- Tab bar: Extension's
sidepanel.htmlprovides Studio/Observatory switching via separate iframes
URL Parameters Reference
When gograb-ai is loaded in an iframe, it reads parameters from the URL to configure its behavior.
/panel Route (Studio)
| Parameter | Type | Default | Description |
|---|---|---|---|
embedded |
'true' / 'false' |
'false' |
Signals the app is running inside an iframe. Hides standalone header, adjusts background. |
extension |
'true' / 'false' |
'false' |
Signals Chrome extension context. Triggers URL-param auth flow instead of waiting for bridge auth. |
theme |
'dark' / 'light' |
'dark' |
Visual theme override for the panel. |
userRole |
'developer' / 'builder' / 'user' |
'user' |
Maps to tenant tier: developer = T1 (full access), builder = T2 (space builder), user = T3 (platform user). |
hostApp |
string |
— | Identifies the host application: 'space', 'admin', 'dev', 'ai', 'space-admin'. |
token |
string |
— | JWT access token for authentication. Used in extension context where bridge auth is unreliable. |
userId |
string |
— | User ID for authentication context. |
userEmail |
string |
— | User email for display and identification. |
userName |
string |
— | User display name. |
spaceId |
string |
— | Current space context. Enables space-scoped operations and permissions. |
source |
string |
— | Origin identifier: 'embed-component' (from Angular bridge), 'sidepanel' (from extension). |
instanceId |
string |
— | Optional instance ID for multi-instance support. |
Example URL:
http://localhost:6400/panel?embedded=true&extension=true&theme=dark&userRole=developer
This URL means:
localhost:6400-- Thegograb-aiapp running on thegspace1workspace (port 6400)/panel-- Loading the Studio routeembedded=true-- Running inside an iframe (hides standalone header)extension=true-- Running inside the Chrome extension (uses URL-param auth)theme=dark-- Dark themeuserRole=developer-- Full T1 developer access
/observatory Route
| Parameter | Type | Default | Description |
|---|---|---|---|
embedded |
'true' / 'false' |
'false' |
Same as panel. |
extension |
'true' / 'false' |
'false' |
Same as panel. |
theme |
'dark' / 'light' |
'dark' |
Visual theme. |
mode |
'compact' / 'full' |
'compact' |
compact = single-column (side panel), full = multi-column dashboard. |
controls |
'true' / 'false' |
'true' |
Show/hide control buttons (submit task, etc.). |
spaceId |
string |
— | Space context. |
source |
string |
— | Origin identifier. |
token, userId, userEmail, userName |
string |
— | Auth (same as panel). |
Example URL:
http://localhost:6400/observatory?embedded=true&mode=compact&controls=true&source=embed-component
Workspace Port Derivation
Each development workspace occupies a 1000-port range. The AI app is always at offset +400:
| Workspace | Port Range | AI Port | Example Host |
|---|---|---|---|
editor2 |
5000-5999 | 5400 | gograb-space at 5200 |
gspace1 |
6000-6999 | 6400 | gograb-space at 6200 |
gograb-modules |
4000-4999 | 4400 | gograb-admin at 4201 |
How deriveAiPort() Works
Located in libs/libs-ux/embeddable-bridge/src/lib/utils/workspace-ports.ts:
const AI_PORT_OFFSET = 400;
export function deriveAiPort(fallbackPort = 5400): number {
if (typeof window === 'undefined') return fallbackPort;
const hostPort = parseInt(window.location.port, 10);
if (isNaN(hostPort) || hostPort < 4000 || hostPort > 9999) {
return fallbackPort;
}
const workspaceBase = Math.floor(hostPort / 1000) * 1000;
return workspaceBase + AI_PORT_OFFSET;
}
Logic:
- Read the current browser window's port (e.g.,
6200) - Compute the workspace base:
Math.floor(6200 / 1000) * 1000 = 6000 - Add the AI offset:
6000 + 400 = 6400
Fallback: If running in SSR, or port is outside 4000-9999, returns fallbackPort (default: FRONTEND_PORTS.gograbAi = 5400).
Extension Port Resolution
The Chrome extension cannot use deriveAiPort() because it runs in its own context (no Angular host port). Instead, sidepanel.js probes known ports sequentially:
const KNOWN_AI_PORTS = [5400, 6400, 4200];
async function resolveAiAppUrl() {
for (const port of KNOWN_AI_PORTS) {
try {
const resp = await fetch(`http://localhost:${port}/`, { mode: 'no-cors' });
return `http://localhost:${port}`;
} catch { continue; }
}
}
Bridge Event Model
All communication between host and embedded app uses typed BridgeMessage objects sent via window.postMessage:
interface BridgeMessage<T = unknown> {
type: BridgeMessageType;
payload?: T;
id?: string;
timestamp: number;
origin?: string;
instanceId?: string;
}
Message Categories
Lifecycle
| Type | Direction | Description |
|---|---|---|
AI_APP_READY |
Embedded -> Host | App is loaded and ready to receive messages |
AI_APP_DESTROYED |
Embedded -> Host | App is being destroyed |
AI_PING |
Host -> Embedded | Heartbeat check |
AI_PONG |
Embedded -> Host | Heartbeat response |
AI_ACK |
Both | Acknowledgment of a received message |
AI_HEARTBEAT |
Host -> Embedded | Periodic keep-alive |
AI_RECONNECT |
Host -> Embedded | Request to re-establish connection |
Authentication
| Type | Direction | Description |
|---|---|---|
AI_AUTH_INIT |
Host -> Embedded | Send initial auth credentials |
AI_AUTH_REFRESH |
Host -> Embedded | Send refreshed token |
AI_AUTH_LOGOUT |
Host -> Embedded | Notify logout |
AI_AUTH_REQUEST_REFRESH |
Embedded -> Host | Request token refresh |
Context
| Type | Direction | Description |
|---|---|---|
AI_CONTEXT_UPDATE |
Host -> Embedded | Send updated page/feature context |
AI_CONTEXT_REQUEST |
Embedded -> Host | Request current context |
Drag and Drop
| Type | Direction | Description |
|---|---|---|
AI_DRAG_START |
Embedded -> Host | User started dragging an output |
AI_DRAG_MOVE |
Embedded -> Host | Drag position update |
AI_DRAG_END |
Embedded -> Host | Drag ended (with drop data) |
AI_DROP_SUCCESS |
Host -> Embedded | Drop was accepted by host canvas |
AI_DROP_CANCELLED |
Host -> Embedded | Drop was cancelled/rejected |
Prompts and Responses
| Type | Direction | Description |
|---|---|---|
AI_PROMPT |
Host -> Embedded | Send a prompt to the AI |
AI_RESPONSE |
Embedded -> Host | AI response complete |
Streaming
| Type | Direction | Description |
|---|---|---|
AI_STREAM_START |
Embedded -> Host | Streaming response started |
AI_STREAM_CHUNK |
Embedded -> Host | Streaming chunk received |
AI_STREAM_END |
Embedded -> Host | Streaming completed |
AI_STREAM_ERROR |
Embedded -> Host | Streaming error |
Panel State
| Type | Direction | Description |
|---|---|---|
AI_PANEL_COLLAPSE |
Host -> Embedded | Panel was collapsed |
AI_PANEL_EXPAND |
Host -> Embedded | Panel was expanded |
AI_PANEL_STATE |
Both | Panel state synchronization |
Errors
| Type | Direction | Description |
|---|---|---|
AI_ERROR |
Embedded -> Host | General error |
AI_ERROR_BOUNDARY |
Embedded -> Host | Component error boundary triggered |
Authentication Flows
Flow A: URL Parameters (Extension)
Used when extension=true is in the URL. This is the primary auth mechanism for the Chrome extension because postMessage auth can be unreliable in the extension context.
Chrome Extension gograb-ai App
| |
| iframe src=".../panel? |
| extension=true& |
| token=JWT&userId=X& |
| userEmail=Y" |
|---------------------------------->|
| |
| initializeFromExtensionParams()
| reads token, userId, userEmail
| from URL, sets auth state
| |
| AI_APP_READY |
|<----------------------------------|
Flow B: Bridge PostMessage (Angular Embeds)
Used by GoGrabAiPanelComponent and GoGrabObservatoryComponent when embedding in Angular apps.
Host Angular App gograb-ai App (iframe)
| |
| <iframe src=".../panel? |
| embedded=true"> |
|---------------------------------->|
| |
| AI_APP_READY |
|<----------------------------------|
| |
| If autoSendOnReady: |
| AI_AUTH_INIT { token, user } |
|---------------------------------->|
| sets auth state from payload
| |
| ...later... |
| AI_AUTH_REQUEST_REFRESH |
|<----------------------------------|
| |
| AI_AUTH_REFRESH { newToken } |
|---------------------------------->|
Flow C: Shared Session (Direct Component)
When using <lib-ai-panel> directly in gograb-space, there is no bridge. The panel runs in the same Angular injector tree and shares the same AuthService, HttpClient, and cookie-based session. No special auth wiring needed.
Component Reference
EmbeddableHostComponent
Selector: embeddable-host
Library: @libs-ux/embeddable-bridge
Role: Creates and manages the iframe, wires up the bridge.
| Input | Type | Default | Description |
|---|---|---|---|
embeddedUrl |
string |
— | Full URL to load in iframe |
allowedOrigins |
string[] |
[] |
Origins allowed for PostMessage |
width |
string |
'100%' |
Iframe width |
height |
string |
'100%' |
Iframe height |
title |
string |
'Embedded App' |
Iframe title (accessibility) |
enableDragDrop |
boolean |
false |
Enable drag-drop from iframe |
enableHeartbeat |
boolean |
true |
Enable heartbeat monitoring |
autoSendAuth |
boolean |
false |
Auto-send auth on AI_APP_READY |
authGetter |
() => AuthPayload |
null |
Function to get current auth |
| Output | Type | Description |
|---|---|---|
onReady |
void |
Iframe app is ready |
onMessage |
BridgeMessage |
Any bridge message received |
onDrop |
DragPayload |
Drag-drop completed |
onError |
Error |
Bridge error occurred |
onDisconnect |
void |
Heartbeat timeout |
onReconnect |
void |
Connection re-established |
GoGrabAiPanelComponent
Selector: gograb-ai-panel
Role: High-level wrapper that builds the /panel URL with workspace-aware port and passes it to EmbeddableHostComponent.
| Input | Type | Default | Description |
|---|---|---|---|
context |
string |
'Dashboard' |
Feature context for AI suggestions |
spaceId |
string |
'' |
Space ID for scoped operations |
tier |
'T1' / 'T2' / 'T3' |
'T3' |
User tier (maps to userRole URL param) |
theme |
'dark' / 'light' |
'dark' |
Theme override |
enableDragDrop |
boolean |
true |
Enable drag-drop |
authGetter |
() => AuthPayload |
null |
Auth function |
aiBaseUrl |
string |
null |
Override auto-detected base URL |
GoGrabObservatoryComponent
Selector: gograb-observatory
Role: High-level wrapper that builds the /observatory URL.
| Input | Type | Default | Description |
|---|---|---|---|
mode |
'compact' / 'full' |
'compact' |
Display mode |
showControls |
boolean |
true |
Show control buttons |
spaceId |
string |
'' |
Space ID |
authGetter |
() => AuthPayload |
null |
Auth function |
aiBaseUrl |
string |
null |
Override auto-detected base URL |
AiPanelTabsComponent (Session 105.3)
Selector: ai-panel-tabs
Library: @libs-ai/vitto-hub
Role: Wraps AIPanelComponent and AgentObservatoryComponent with tab switching for native embedding.
| Input | Type | Default | Description |
|---|---|---|---|
context |
string |
'' |
Pass-through to Studio |
tenantId |
string |
'' |
Pass-through to Studio |
spaceId |
string |
'' |
Pass-through to Studio |
observatoryMode |
'compact' / 'full' |
'compact' |
Observatory display mode |
observatoryControls |
boolean |
true |
Observatory control visibility |
| Output | Type | Description |
|---|---|---|
close |
void |
Panel close requested |
capabilityUsed |
AIPanelCapability |
AI capability was used |
messageSent |
AIChatMessage |
Message sent to AI |
outputDragged |
ActionableOutput |
Output dragged from panel |
outputExecuted |
ActionableOutput |
Output executed in panel |
Comparison Table
| Aspect | Direct (Native) | Embeddable Bridge | Chrome Extension |
|---|---|---|---|
| Component | <ai-panel-tabs> |
<gograb-ai-panel> |
sidepanel.js iframe |
| Location | Same Angular app | Different Angular app | Browser extension |
| Communication | Angular I/O bindings | PostMessage bridge | PostMessage (vanilla JS) |
| Auth | Shared DI tree | Bridge AI_AUTH_INIT |
URL params (primary) |
| Port | N/A (same app) | deriveAiPort() |
Runtime port probing |
| Tab Switching | AiPanelTabsComponent |
N/A (host decides) | sidepanel.html tab bar |
| Drag-Drop | Direct events | Bridge AI_DRAG_* |
Not supported |
| Heartbeat | N/A | AI_PING/AI_PONG |
AI_PING/AI_PONG |
| Use Cases | gograb-space |
gograb-admin, gograb-dev |
Chrome side panel |
Troubleshooting
ERR_CONNECTION_REFUSED
Symptom: Iframe shows "localhost refused to connect"
Cause: The gograb-ai app is not running on the expected port.
Fix:
- Verify the app is running:
yarn serve:gograb-ai - Check the port in
.env.localmatches the workspace (e.g.,GOGRAB_AI_PORT=6400forgspace1) - Never run
npx nx serve gograb-aidirectly -- useyarn serve:gograb-aiwhich reads.env.localviadev-serve.js
Blank Panel (No Content)
Symptom: Iframe loads but shows nothing
Causes:
- Missing Angular providers. Check browser console for
NG0201: No providererrors - Missing
provideLogSystem()inapp.config.ts(Session 105.3 fix)
Icons Show as Text
Symptom: Mode tabs show "chat", "image", "videocam" as plain text
Cause: Material Icons font not loaded in gograb-ai
Fix: Ensure index.html has:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Chrome PNA Preflight (Private Network Access)
Symptom: 404 Not Found on OPTIONS requests when embedding from extension
Cause: Chrome requires Access-Control-Allow-Private-Network: true header for local network access
Fix: The apps/gograb-ai/proxy.conf.js handles this with a /** catch-all that responds to OPTIONS with PNA headers.
WebSocket 400 Error on Backend
Symptom: Backend WebSocket connections fail with HTTP 400
Cause: --port flag was passed to a backend @nx/js:node executor. This sets the Node.js inspector port, not the app port, causing the inspector to intercept WebSocket upgrade requests.
Fix: Never pass --port to backend serve commands. Use yarn serve:backend-ai which reads the correct port from .env.local.
Observatory "Connected: No"
Symptom: Observatory panel shows "Connected: No" and "Error"
Cause: VittoMonitorService uses EventSource (SSE) which cannot send custom HTTP headers. If the backend requires Authorization, the SSE connection gets 401 Unauthorized.
Fix: Either configure the backend to accept cookie-based auth for SSE, or implement a token-in-URL scheme for the SSE endpoint.
File Reference
| File | Purpose |
|---|---|
libs/libs-ux/embeddable-bridge/src/lib/components/embeddable-host.component.ts |
Host iframe container and adapter wiring |
libs/libs-ux/embeddable-bridge/src/lib/components/gograb-ai-panel.component.ts |
Builds /panel URL with workspace port |
libs/libs-ux/embeddable-bridge/src/lib/components/gograb-observatory.component.ts |
Builds /observatory URL with workspace port |
libs/libs-ux/embeddable-bridge/src/lib/utils/workspace-ports.ts |
deriveAiPort() function |
libs/libs-ux/embeddable-bridge/src/lib/models/bridge-events.model.ts |
Message types and payload interfaces |
libs/libs-ux/embeddable-bridge/src/lib/services/host-adapter.service.ts |
Host-side bridge (send/receive/heartbeat) |
libs/libs-ux/embeddable-bridge/src/lib/services/embedded-adapter.service.ts |
Embedded-side bridge (receive/reply/URL auth) |
apps/gograb-ai/src/app/pages/extension-panel/extension-panel.component.ts |
/panel route, parses URL params |
apps/gograb-ai/src/app/pages/observatory/observatory-page.component.ts |
/observatory route, parses URL params |
apps/gograb-ai-extension/sidepanel/sidepanel.js |
Extension side panel, iframe URL building |
apps/gograb-ai/proxy.conf.js |
PNA preflight handler for Chrome embedding |
libs/libs-ai/vitto-hub/src/lib/components/ai-panel-tabs/ |
Studio/Observatory tab switcher (Session 105.3) |