Heimdall Docs

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 DOM

2. 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 messages

3. 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 updates

4. 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 it

5. 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 notifications

8. 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

1. The page or layout subscribes to the topic.
2. The action publishes HTML to that topic.
3. Heimdall.js receives the SSE message.
4. The payload is applied to the configured target and swap mode.

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 begins

13. 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 flow

14. 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 payloads

16. 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 click

17. 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 it