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-jsxresources. - 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:
- Injects a
<script>tag referencing the Symphony bridge library (symphony-bridge.js) into the HTML. - Loads the result into a sandboxed iframe with
allow-scripts allow-same-origin allow-formspermissions. - 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:
| Property | Type | Description |
|---|---|---|
data | string | The response payload (UTF-8 decoded) |
subject | string | The 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:
| Property | Type | Description |
|---|---|---|
key | string | The key name |
value | string | The stored value |
revision | number | The 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)
Navigation
Navigate
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:
| Property | Type | Description |
|---|---|---|
symphonyInfo.name | string | Platform name |
symphonyInfo.shortname | string | Short platform name |
symphonyInfo.version | string | Platform version |
user.name | string | Current user's display name |
user.email | string | Current user's email |
accountIdentifier | string | Account identifier |
roles | string[] | User's assigned roles |
colorMode | 'light' | 'dark' | Current theme mode |
isConnected | boolean | Whether 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
| Aspect | text/symphony-jsx | text/html+symphony |
|---|---|---|
| Language | React/TypeScript (JSX) | Any HTML/JS |
| API access | Direct (SymphonyContext) | Bridge (window.symphony) |
| Module resolution | Automatic via /npm CDN | Self-managed |
| Theming | MUI automatic sync | Manual via onContextChange |
| Build step | None (browser transpilation) | Optional (your choice) |
| Routing | React Router (synced) | Manual via navigate/onLocationChange |
See Also
- User Interfaces—Overview of all resource types
- Symphony JSX—React/TypeScript resource type