bext.dev
DocsServer-Side Includes
Server-Side Includes4 min read
On this page

Realtime includes

An include can subscribe to a realtime channel. On first load it renders server-side like any other include; after that, a client-side SSE subscription re-fetches the fragment whenever the server publishes to the channel.

html
<bext-include src="/partials/order-status"
  realtime="orders:123"
></bext-include>

No framework, no state management. Just a <div> that updates itself when the server says it should.

Attributes#

Attribute Purpose
realtime Channel/topic name to subscribe to
realtime-mode replace (default), append, prepend
realtime-event Filter to a specific event type

How it works#

On SSR: The include resolves normally — full server-side HTML. bext wraps the result in a container with realtime metadata:

html
<div data-bext-realtime="orders:123"
     data-bext-include-src="/partials/order-status"
     data-bext-realtime-mode="replace">
  <p>Order #123: Preparing</p>
</div>

On the client: bext injects a tiny inline script (once per page) that discovers all [data-bext-realtime] elements and subscribes to their channels via EventSource:

javascript
// Injected automatically when any realtime include is present
var es = new EventSource('/__bext/realtime/sse?topic=orders:123');
es.onmessage = function(ev) {
  // Re-fetch the include HTML
  fetch('/__bext/include?src=/partials/order-status')
    .then(r => r.text())
    .then(html => {
      el.innerHTML = html;  // or append/prepend based on mode
    });
};

On publish: Any server-side code that updates the underlying data publishes to the channel:

rust
realtime_hub.publish("orders:123", json!({
    "event": "order-update",
    "status": "shipped"
}));

All subscribed clients re-fetch the include automatically.

Update modes#

Control how updates apply to the DOM:

html
<!-- Replace: new content replaces current (default) -->
<bext-include src="/partials/status"
  realtime="orders:123"
  realtime-mode="replace"
></bext-include>

<!-- Append: new content added to the end (live feeds) -->
<bext-include src="/partials/latest-message"
  realtime="chat:42"
  realtime-mode="append"
></bext-include>

<!-- Prepend: new content added to the start (notifications) -->
<bext-include src="/partials/notification"
  realtime="user:42:notifs"
  realtime-mode="prepend"
></bext-include>

Event filtering#

A channel can carry multiple event types. Use realtime-event to subscribe to just one:

html
<!-- Only re-render when the server publishes event="price-change" -->
<bext-include src="/partials/price-tag"
  realtime="stock:AAPL"
  realtime-event="price-change"
></bext-include>

<!-- Only re-render when event="volume-update" -->
<bext-include src="/partials/volume-meter"
  realtime="stock:AAPL"
  realtime-event="volume-update"
></bext-include>

Both subscribe to stock:AAPL but react to different events. The server publishes:

rust
realtime_hub.publish("stock:AAPL", json!({
    "event": "price-change",
    "price": 171.25
}));

Only the price-tag include re-fetches. The volume-meter ignores this event.

Tip

Multiple includes sharing the same channel on a page open one EventSource connection, not one per include. When a publish fires, all of them re-fetch via the /__bext/include/batch endpoint in a single round trip.

Channel authorization#

The realtime hub enforces per-channel auth rules (same system as other realtime subscriptions):

toml
[realtime.auth]
# User-specific channels require authentication
"user:*" = { auth = "required" }

# Admin-only channels
"admin:*" = { auth = "required", role = "admin" }

# Public channels — no auth needed
"public:*" = { auth = "none" }

Unauthorized subscribers receive 403 Forbidden from the SSE endpoint. The include's SSR still happens (subject to the include's own auth attribute) — only the live subscription is gated.

Warning

The realtime channel authorizes the live subscription separately from the include's own auth attribute. A public channel (auth = "none") can still be used with an auth="required" include — but the subscription itself will be open to anyone.

Pairing with caching#

A realtime include's initial SSR can still be cached with a short TTL to absorb request bursts:

html
<bext-include src="/partials/live-scores"
  realtime="scores:match-42"
  ttl="5s"
></bext-include>
  • The first request resolves and caches for 5 seconds.
  • Subsequent initial page loads (during the 5s window) hit cache.
  • The client-side subscription keeps the content fresh after load.

Don't use long TTLs — the cache is a safety net, not the source of truth. The realtime channel is what keeps the content fresh.

Connection management#

Multiple includes on the same page sharing the same channel share a single connection:

html
<bext-include src="/partials/order-header" realtime="orders:123"></bext-include>
<bext-include src="/partials/order-timeline" realtime="orders:123"></bext-include>
<bext-include src="/partials/order-total" realtime="orders:123"></bext-include>

One EventSource is opened for orders:123; all three includes listen on it and re-fetch when an event arrives. The batch endpoint /__bext/include/batch resolves all three in a single request:

code
POST /__bext/include/batch
[
  { "src": "/partials/order-header" },
  { "src": "/partials/order-timeline" },
  { "src": "/partials/order-total" }
]

Returns an array of resolved HTML strings, each swapped into its respective element.

Client-side reconnection#

The injected script handles automatic reconnection:

  • On disconnect, it waits 1-5 seconds (exponential backoff) and reconnects.
  • On the server side, bext's realtime hub resumes the subscription seamlessly.
  • In-flight events that occurred during the disconnect are replayed if the client sends a Last-Event-ID header (standard SSE behavior).

Performance#

  • One EventSource per unique channel per page (not per include).
  • Re-fetch uses /__bext/include?src=... which respects the include's cache.
  • Server-side publishes fan out via the realtime hub's internal pub/sub.
  • With Redis backend, publishes propagate across horizontally-scaled instances.

Example: live dashboard#

html
<body>
<h1>Dashboard</h1>

<section>
  <h2>Active orders</h2>
  <bext-include src="/partials/active-order-count"
    realtime="system:orders"
    ttl="10s"
  ></bext-include>
</section>

<section>
  <h2>Recent events</h2>
  <bext-include src="/partials/event-feed"
    realtime="system:events"
    realtime-mode="prepend"
  >Loading events...</bext-include>
</section>

<section>
  <h2>Server health</h2>
  <bext-include src="/partials/health-metrics"
    realtime="system:health"
    realtime-event="alert"
    ttl="30s"
  ></bext-include>
</section>
</body>

Three different channels, three different update modes, all self-updating. No dashboard framework, no WebSocket management code.

See also#

Edit this page ↗Need a hand? ↗
FIND YOUR NEXT STEP

Start with a topic, a command, or a question.