Skip to main content

HTML + Symphony Bridge Resources

The text/html+symphony MIME type lets you write standard HTML, CSS, and JavaScript while accessing Symphony platform APIs through a window.symphony bridge object. This provides the same NATS messaging, KV storage, navigation, and context capabilities as text/symphony-jsx, but without requiring React or TypeScript.

When to Use

Use text/html+symphony when you want access to Symphony platform APIs but prefer to use standard HTML, CSS, and JavaScript—or a framework other than React. This resource type is ideal when:

  • You want to use a non-React framework such as Vue, Svelte, or Angular, while still accessing NATS messaging and KV storage.
  • Your application depends on specific library versions that may conflict with the versions injected by Symphony for text/symphony-jsx resources.
  • You have an existing web application that you want to integrate with Symphony without rewriting it as React components.
  • You prefer to use your own build toolchain and bundle your own dependencies.

If you do not need access to Symphony APIs, use text/html instead for a simpler setup.

How It Works

When Symphony receives a text/html+symphony resource, it:

  1. Injects a <script> tag referencing the Symphony bridge library (symphony-bridge.js) into the HTML.
  2. Loads the result into a sandboxed iframe with allow-scripts allow-same-origin allow-forms permissions.
  3. Establishes a postMessage-based handshake between the iframe and the parent Symphony UI.

Once the handshake completes, the window.symphony object is fully functional and symphony.ready resolves. All API calls are proxied through postMessage to the parent frame, which executes them using the same NATS connection and permissions as the logged-in user.

Registration

html_content = """
<html>
<head><title>My Dashboard</title></head>
<body>
<h1 id="greeting">Loading...</h1>
<script>
symphony.ready.then(async () => {
const ctx = await symphony.getContext()
document.getElementById('greeting').textContent =
'Hello, ' + ctx.user.name
})
</script>
</body>
</html>
"""

ext.add_resource("ui://my_ext/dashboard", "text/html+symphony", html_content)
ext.add_route("/my_ext", "ui://my_ext/dashboard")
Resource dashboard = new Resource()
.uri("ui://my_ext/dashboard")
.mimeType(MimeType.TEXT_HTML_SYMPHONY)
.text(getTemplate("dashboard.html"));

The window.symphony API

All methods are available after symphony.ready resolves.

Initialization

symphony.ready.then(async () => {
// Bridge is connected — all APIs are available
const ctx = await symphony.getContext()
console.log('Connected to', ctx.symphonyInfo.name)
})

NATS Messaging

Request/Reply

Send a request to a NATS subject and wait for a response:

const reply = await symphony.nats.request(
'cirata.extensions.my_ext.query',
JSON.stringify({ filter: 'active' }),
{ timeout: 5000 }
)
const data = JSON.parse(reply.data)

The reply object contains:

PropertyTypeDescription
datastringThe response payload (UTF-8 decoded)
subjectstringThe reply subject

Publish

Publish a message to a NATS subject (fire-and-forget):

await symphony.nats.publish('cirata.extensions.my_ext.event', {
type: 'update',
timestamp: Date.now()
})

Subscribe

Subscribe to messages on a NATS subject:

const sub = await symphony.nats.subscribe(
'cirata.extensions.my_ext.notifications',
(msg) => {
console.log('Received:', msg.subject, msg.data)
}
)

// Later, unsubscribe
await sub.unsubscribe()

KV Storage

Open or Create a Bucket

// Open an existing bucket
const bucket = await symphony.kv.open('my_ext_data')

// Create a bucket (creates if it does not exist)
const bucket = await symphony.kv.create('my_ext_data')

Get, Put, and Delete

// Store a value
await bucket.put('config', JSON.stringify({ theme: 'dark', limit: 50 }))

// Retrieve a value
const entry = await bucket.get('config')
if (entry) {
const config = JSON.parse(entry.value)
console.log('Config:', config)
console.log('Revision:', entry.revision)
}

// Delete a key
await bucket.delete('config')

The entry object returned by get contains:

PropertyTypeDescription
keystringThe key name
valuestringThe stored value
revisionnumberThe entry revision number

Returns null if the key does not exist.

Watch for Changes

Watch a bucket for real-time updates:

const watcher = await bucket.watch(
{ key: 'status' },
(update) => {
console.log('Key:', update.key, 'Value:', update.value)
console.log('Operation:', update.operation) // "PUT", "DEL", "PURGE"
}
)

// Later, stop watching
await watcher.stop()

The opts parameter is optional. Omit it to watch all keys:

const watcher = await bucket.watch((update) => {
console.log('Changed:', update.key)
})

List Keys and Status

const keys = await bucket.keys()
console.log('All keys:', keys)

const status = await bucket.status()
console.log('Bucket:', status.bucket, 'Entries:', status.values, 'Bytes:', status.bytes)

Navigate the parent Symphony UI to a path:

symphony.navigate('/my_ext/settings')

Get Current Location

const location = await symphony.getLocation()
console.log(location.pathname, location.search, location.hash)

Listen for Location Changes

const unsubscribe = symphony.onLocationChange((location) => {
console.log('Navigated to:', location.pathname)
})

// Later, stop listening
unsubscribe()

Page Title

Set the page title displayed in the Symphony breadcrumb navigation. The parameter is a map from route path to label:

symphony.setTitle({
'/my_ext': 'My Extension',
'/my_ext/settings': 'Settings'
})

Context

Get Context

Retrieve information about the current Symphony session:

const ctx = await symphony.getContext()

The context object contains:

PropertyTypeDescription
symphonyInfo.namestringPlatform name
symphonyInfo.shortnamestringShort platform name
symphonyInfo.versionstringPlatform version
user.namestringCurrent user's display name
user.emailstringCurrent user's email
accountIdentifierstringAccount identifier
rolesstring[]User's assigned roles
colorMode'light' | 'dark'Current theme mode
isConnectedbooleanWhether the NATS connection is active

Listen for Context Changes

const unsubscribe = symphony.onContextChange((ctx) => {
if (ctx.colorMode) {
document.body.className = ctx.colorMode
}
})

Complete Example

A self-contained dashboard page that queries a backend service, stores preferences in KV, and responds to theme changes:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Dashboard</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; margin: 0; }
body.dark { background: #121212; color: #e0e0e0; }
table { border-collapse: collapse; width: 100%; margin-top: 16px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
body.dark th, body.dark td { border-color: #444; }
.status { padding: 4px 8px; border-radius: 4px; font-size: 0.85em; }
.status.ok { background: #c8e6c9; color: #2e7d32; }
.status.error { background: #ffcdd2; color: #c62828; }
body.dark .status.ok { background: #1b5e20; color: #a5d6a7; }
body.dark .status.error { background: #b71c1c; color: #ef9a9a; }
</style>
</head>
<body>
<h1 id="title">Loading...</h1>
<p>Connected as <span id="user"></span></p>
<table>
<thead><tr><th>Service</th><th>Status</th></tr></thead>
<tbody id="services"></tbody>
</table>

<script>
symphony.ready.then(async () => {
// Set title
symphony.setTitle({ '/my_ext': 'My Dashboard' })

// Load context
const ctx = await symphony.getContext()
document.getElementById('title').textContent = ctx.symphonyInfo.name + ' Dashboard'
document.getElementById('user').textContent = ctx.user.name
document.body.className = ctx.colorMode

// Listen for theme changes
symphony.onContextChange((update) => {
if (update.colorMode) {
document.body.className = update.colorMode
}
})

// Query service info
try {
const reply = await symphony.nats.request('$SRV.INFO', '', { timeout: 3000 })
const info = JSON.parse(reply.data)
const tbody = document.getElementById('services')
tbody.innerHTML = '<tr><td>' + info.name + '</td>' +
'<td><span class="status ok">Running</span></td></tr>'
} catch (err) {
console.error('Service query failed:', err)
}

// Load saved preferences from KV
try {
const bucket = await symphony.kv.create('my_ext_prefs')
const entry = await bucket.get('dashboard')
if (entry) {
console.log('Loaded preferences:', entry.value)
}
} catch (err) {
console.error('KV access failed:', err)
}
})
</script>
</body>
</html>

Using with Frameworks

Because text/html+symphony resources are standard HTML, you can use any JavaScript framework. Build your application with your preferred toolchain and provide the output HTML:

import os

# Load pre-built HTML from your framework's output
with open(os.path.join(os.path.dirname(__file__), 'dist', 'index.html')) as f:
html = f.read()

ext.add_resource("ui://my_ext/app", "text/html+symphony", html)

The window.symphony API is framework-agnostic and works with Vue, Svelte, Angular, or any other framework that runs in the browser.

Differences from text/symphony-jsx

Aspecttext/symphony-jsxtext/html+symphony
LanguageReact/TypeScript (JSX)Any HTML/JS
API accessDirect (SymphonyContext)Bridge (window.symphony)
Module resolutionAutomatic via /npm CDNSelf-managed
ThemingMUI automatic syncManual via onContextChange
Build stepNone (browser transpilation)Optional (your choice)
RoutingReact Router (synced)Manual via navigate/onLocationChange

See Also