SSE / Bifrost
Bifrost is Heimdall’s server-sent events layer. It lets the server push HTML updates to subscribed browser clients outside the normal request-response cycle.
If out-of-band updates are side effects inside a response, SSE is how Heimdall delivers HTML when there is no active response at all.
1. What SSE Means in Heimdall
With Bifrost, the browser subscribes to a topic and the server can later publish HTML to that topic. The client runtime receives that HTML and applies it using the configured target and swap behavior.
Browser subscribes to a topic
-> server publishes HTML later
-> Heimdall applies that HTML to the DOM2. Why Bifrost Exists
Some UI updates are not the immediate result of the current click or submit. They may come from background work, another action, another user, or some other server event. That is where SSE fits.
Good fits for SSE:
- toast notifications
- status changes
- background job progress
- live dashboards
- shared activity streams
- async completion messages3. The Basic Model
Bifrost uses topic-based subscriptions. A page or layout declares interest in a topic, and the server publishes HTML messages to that topic.
Subscriber -> topic: "toasts"
Publisher -> topic: "toasts"
Result -> subscribed clients receive HTML updates4. Important: Topics Are App-Defined
Topic names are chosen by the application. Heimdall does not automatically scope them per request, per user, or per page. If multiple clients subscribe to the same topic, they can all receive the same published HTML.
topic: "toasts"
-> every subscriber to "toasts" can receive the message
topic: "toasts:user:123"
-> only subscribers to that user topic receive it
topic: "toasts:page:abc123"
-> only that page instance receives it5. Layout-Level Subscription
A common pattern is to mount the SSE subscription in the layout so every page can receive updates for a shared UI region like a toast manager.
Strongly Typed Markup
root.Id("toast-manager");
root.Heimdall()
.SseTopic("toasts")
.SseTarget("#toast-manager")
.SseSwap(Swap.BeforeEnd);That example is intentionally simple, but it is also important to understand what it means: a shared topic like "toasts" behaves like a shared channel. That is fine for demos, shared dashboards, or broadcast-style notifications, but it is not the right default for per-user UI feedback.
Rendered HTML Mental Model
<div
id="toast-manager"
heimdall-sse="toasts"
heimdall-sse-target="#toast-manager"
heimdall-sse-swap="beforeend">
</div>6. Publishing to a Topic
On the server, publishing is straightforward: render HTML and publish it to the topic that clients are subscribed to.
await bifrost.PublishAsync(
topic: "toasts",
payload: ToastManager.Create(toast),
cancellationToken: ct
);7. Safe Defaults
For most immediate user interactions, out-of-band updates are the safer default. OOB updates are tied to the current response, so they naturally affect only the client that made the request.
Prefer OOB for:
- form submissions
- button clicks
- immediate success/error feedback
- request-local side effects
Prefer SSE for:
- async or delayed updates
- background work completion
- live status streams
- intentionally shared notifications8. Topic Scoping Strategies
When using SSE, choose a topic strategy that matches the behavior you want. The best choice depends on whether the update should be visible to one page, one browser session, one user, or many clients.
Common strategies:
Broadcast:
"toasts"
Per-user:
"toasts:user:{userId}"
Per-session:
"toasts:session:{sessionId}"
Per-page:
"toasts:page:{pageId}"9. Full Example
This is the classic Heimdall SSE example: a button triggers an action, the action publishes toast HTML to the toasts topic, and the layout-level toast manager receives it.
That makes the mechanics easy to understand, but remember that a shared topic like toasts should be treated as a shared channel. In a real app, immediate button-click toasts are usually better handled with OOB, or the SSE topic should be scoped intentionally.
Publisher Action
[ContentInvocation]
public static async Task<IHtmlContent> ToastViaSse(
Bifrost bifrost,
CancellationToken ct)
{
var toast = new ToastItem
{
Header = "Hello from SSE!",
Content = "This toast was published to subscribed clients.",
Type = ToastType.Success,
DurationMs = 1800
};
await bifrost.PublishAsync(
topic: "toasts",
payload: ToastManager.Create(toast),
cancellationToken: ct
);
return HtmlString.Empty;
}Trigger
button.Heimdall()
.Click("OobPage.ToastViaSse")
.SwapNone();What Happens
10. SSE Uses HTML Too
Bifrost does not switch Heimdall into a JSON event protocol. The payload is still HTML, which means the same rendering model works for pages, actions, OOB, and streaming.
await bifrost.PublishAsync(
topic: "orders",
payload: RenderOrderRow(order),
cancellationToken: ct
);11. SSE Can Also Carry OOB Instructions
Because the payload is HTML, SSE messages can also include invocation elements. This means a streamed message can update its main target and also perform out-of-band work.
await bifrost.PublishAsync(
topic: "toasts",
payload: HeimdallHtml.Invocation(
targetSelector: "#toast-manager",
swap: HeimdallHtml.Swap.AfterBegin,
payload: ToastManager.Create(toast)
),
cancellationToken: ct
);12. Topic Security
Bifrost subscriptions are not anonymous free-for-alls. The client obtains a short-lived subscribe token before connecting, and that token is tied to the topic.
Client flow:
1. request subscribe token
2. connect to Bifrost with topic + token
3. server validates the token
4. subscription begins13. Why This Matters
This keeps topic subscription explicit and server-controlled. The browser does not just open an unrestricted EventSource to arbitrary topics.
Bifrost subscribe tokens are:
- short-lived
- topic-bound
- obtained through Heimdall’s server flow14. SSE vs Out-of-Band
These patterns are related, but they solve different timing problems.
Out-of-Band
- Comes back inside the current action response
- Immediate result of a user interaction
- Great for local side effects
- No open stream required
- Natural default for request-local toasts
SSE / Bifrost
- Arrives outside the current response
- Can be triggered by later server events
- Great for live and async updates
- Requires an active subscription
- Topic scope should be chosen intentionally
15. Runtime Behavior
The client runtime manages the EventSource connection, reconnect behavior, topic token retrieval, and DOM application of streamed HTML. Subscriptions tied to removed or hidden elements are cleaned up automatically.
Heimdall.js handles:
- subscribe token retrieval
- EventSource connection
- reconnect behavior
- target + swap application
- invocation processing inside SSE payloads16. When to Reach for SSE
Use Bifrost when the update should arrive later, asynchronously, or for more than one subscribed client.
Reach for SSE when:
- a background job completes later
- progress or status should stream
- multiple clients should see the same event
- the update should not wait for another click17. Common Pattern: Toasts
Toasts are one of the clearest examples because they can be delivered either by OOB or by SSE. OOB is great for immediate interaction side effects. SSE is great for async or intentionally shared notifications.
OOB toast:
- user clicks save
- response includes invocation for #toast-manager
SSE toast:
- server publishes to topic "toasts"
- all subscribers to that topic receive itNext Steps
Once SSE makes sense, lazy loading and patterns become much easier to understand.