Configuration
Heimdall has two configuration surfaces: server registration settings and the browser runtime object exposed as window.Heimdall.
Use this page when you need the exact defaults, the mutable runtime properties, and the timing rules for when a setting must be applied.
1. Configuration Surfaces
Most application configuration happens on the server. Browser behavior that is intentionally runtime-tunable lives under Heimdall.config after the JavaScript bundle loads.
Server:
builder.Services.AddHeimdall(options => { ... });
app.UseHeimdall();
Browser:
window.Heimdall
window.Heimdall.config2. Server Settings
AddHeimdall accepts HeimdallServiceSettings. These settings affect server endpoint behavior, error detail, and Bifrost topic authorization.
builder.Services.AddHeimdall(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.BifrostTopicPolicy = "BifrostTopic";
options.AuthorizeBifrostTopic = (ctx, topic) =>
ValueTask.FromResult(topic.StartsWith("user:"));
options.BifrostHeartbeatInterval = TimeSpan.FromSeconds(15);
});3. Server Defaults
These are the framework defaults from HeimdallServiceSettings.
EnableDetailedErrors:
false
BifrostTopicPolicy:
null
AuthorizeBifrostTopic:
null
BifrostHeartbeatInterval:
15 seconds4. Browser Runtime Object
After the bundle loads, Heimdall assigns a global runtime object to window.Heimdall. Its methods are the supported browser-side API surface.
Heimdall.apiVersion
Heimdall.pluginApiVersion
Heimdall.invoke(invocationId, payload, options)
Heimdall.boot(root)
Heimdall.dispose(root)
Heimdall.use(plugin, options)
Heimdall.plugins.list()
Heimdall.plugins.remove(id)
Heimdall.onReady(callback)
Heimdall.clearCsrfToken()
Heimdall.sse.connect(topic, options)
Heimdall.sse.disconnect(element)
Heimdall.sse.disconnectAll()
Heimdall.config
Heimdall._observerOOB Default
Out-of-band response directive processing is enabled by default. The runtime default is Heimdall.config.oobEnabled = true. Set it to false only when the application wants to ignore <invocation> directives returned by invocations or SSE payloads.
5. Set Runtime Config
The runtime creates Heimdall.config when the script loads. Put configuration immediately after the runtime script, and put early-boot options before DOMContentLoaded or before any manual boot call. The hosted Heimdall templates include wwwroot/js/heimdall.config.js and load it after the runtime bundle, giving application-wide defaults one obvious place to live. The static-site generator does not include the browser runtime, so it does not need this hook.
<!-- Shared hosted layout or Components/App.razor -->
<script src="/_content/HeimdallFramework.Web/heimdall-bundle.min.js"></script>
<script src="/js/heimdall.config.js"></script>
// wwwroot/js/heimdall.config.js
(function () {
"use strict";
if (!window.Heimdall)
return;
// Keep the starter file quiet by default.
// Heimdall.config.debug = true;
// Heimdall.config.requestTimeoutMs = 10000;
// Heimdall.config.requestSync = "queue-latest";
// Heimdall.config.clientInfo = true;
})();6. Client Information Storage Limits
The browser configuration controls which localStorage and sessionStorage keys are collected. The server settings apply the same limits while binding the untrusted X-Heimdall-Client-Info header. Keep both sides bounded; the browser skips over-limit values and the server rejects an invalid header.
builder.Services.AddHeimdall(options =>
{
options.ClientInfoLimits.MaxStorageValueLength = 1024;
options.ClientInfoLimits.MaxStorageChars = 2048;
});
Heimdall.config.clientInfo = {
enabled: true,
storage: {
local: ["ui.theme"],
session: ["checkout.state"]
}
};7. Runtime Config Defaults
These are the current browser runtime defaults from startup.js.
Heimdall.config = {
basePath: "/__heimdall",
apiVersion: 1,
endpoints: {
contentInvocations: "/__heimdall/v1/content/invocations",
csrf: "/__heimdall/v1/csrf",
bifrostToken: "/__heimdall/v1/bifrost/token",
bifrost: "/__heimdall/v1/bifrost"
},
observeDom: true,
debug: false,
authReturnUrlParameter: "ReturnUrl",
antiforgery: true,
requestHeaders: null,
clientInfo: false,
clientInfoMaxAgeMs: 60000,
clientInfoLimits: {
maxKeysPerArea: 16,
maxKeyLength: 128,
maxValueLength: 1024,
maxStorageChars: 2048,
maxExtensionEntries: 16,
maxExtensionChars: 1024,
maxExtensionDepth: 4
},
inputDebounceMs: 250,
hoverDelayMs: 150,
scrollThresholdPx: 120,
scrollMinIntervalMs: 250,
visibleRootMargin: "0px",
visibleThreshold: 0,
oobEnabled: true,
requestSync: "parallel",
requestTimeoutMs: 0,
sseDefaultSwap: "none",
sseEventName: "heimdall",
sseReconnectDelayMs: 250,
sseReconnectMaxDelayMs: 10000,
sseReconnectBackoffFactor: 2,
sseSweepIntervalMs: 5000,
ssePauseWhenHidden: false
};8. Endpoint and Auth Redirect Configuration
Endpoint values are read when the runtime makes a request. The default paths match app.UseHeimdall(). Only change them when the server has been mapped behind a matching path or proxy. A separate frontend can use absolute backend URLs, provided the backend exposes the endpoints and the browser is configured for its CORS and authentication policy.
Heimdall.config.endpoints.contentInvocations = "/internal/heimdall/invocations";
Heimdall.config.endpoints.csrf = "/internal/heimdall/csrf";
Heimdall.config.endpoints.bifrostToken = "/internal/heimdall/bifrost/token";
Heimdall.config.endpoints.bifrost = "/internal/heimdall/bifrost";// Separate frontend and HTML backend.
Heimdall.config.endpoints.contentInvocations =
"https://html.example.com/__heimdall/v1/content/invocations";
Heimdall.config.endpoints.csrf =
"https://html.example.com/__heimdall/v1/csrf";
Heimdall.config.endpoints.bifrostToken =
"https://html.example.com/__heimdall/v1/bifrost/token";
Heimdall.config.endpoints.bifrost =
"https://html.example.com/__heimdall/v1/bifrost";Content invocations send POST requests with the invocation id in the query string and X-Heimdall-Content-Invocation header. CSRF and Bifrost token requests are GET requests; they are same-origin only when the configured endpoints are relative.
When cookie authentication redirects an invocation or Bifrost subscribe-token request to sign-in, Heimdall rewrites the configured return URL query parameter to the current page. The default is ReturnUrl, matching ASP.NET Core cookie auth. Matching is case-insensitive, so returnUrl and ReturnUrl are both recognized, and Heimdall preserves the casing used by the server redirect URL.
// ASP.NET Core default:
options.ReturnUrlParameter = "ReturnUrl";
// Heimdall default:
Heimdall.config.authReturnUrlParameter = "ReturnUrl";
// If your app uses a custom name:
options.ReturnUrlParameter = "next";
Heimdall.config.authReturnUrlParameter = "next";9. Trigger Timing Defaults
These config values supply runtime defaults. Per-element attributes and programmatic invocation options still win when they are supplied.
Heimdall.config.inputDebounceMs
-> default for heimdall-content-input
-> overridden by heimdall-debounce
Heimdall.config.hoverDelayMs
-> default for heimdall-content-hover
-> overridden by heimdall-hover-delay
Heimdall.config.scrollThresholdPx
-> near-end scroll distance
-> overridden by heimdall-scroll-threshold
Heimdall.config.scrollMinIntervalMs
-> minimum time between scroll-triggered invocations
Heimdall.config.requestSync
-> default overlap strategy: parallel
-> overridden by heimdall-sync or invoke({ sync })
Heimdall.config.requestTimeoutMs
-> client request timeout: 0 means no timeout
-> overridden by invoke({ timeoutMs })10. Boot and DOM Observation
The runtime boots document boundaries on ready. observeDom installs a MutationObserver so newly inserted Heimdall markup is activated automatically.
Heimdall.config.observeDom = true;
// Manual boot is useful when observeDom is false,
// or when an external renderer inserts markup.
Heimdall.boot(document.querySelector("#new-region"));11. Visible Trigger Timing
visibleRootMargin and visibleThreshold are read once when the IntersectionObserver is first created. Set them before visible elements are booted.
<script src="/_content/HeimdallFramework.Web/heimdall-bundle.min.js"></script>
<script>
Heimdall.config.visibleRootMargin = "200px 0px";
Heimdall.config.visibleThreshold = 0.1;
</script>12. OOB Configuration
OOB processing controls whether returned invocation directives can update other DOM regions outside the main target. Redirect and abort directives are still response directives, but invocation processing is gated by oobEnabled.
// Default: true
Heimdall.config.oobEnabled = true;
// Disable invocation directive processing.
Heimdall.config.oobEnabled = false;13. SSE Configuration
Bifrost SSE uses global defaults for event name, default swap mode, reconnect behavior, sweep timing, and hidden-tab pause behavior. Subscriber attributes can override event, target, swap, and disable behavior per element.
Heimdall.config.sseDefaultSwap = "none";
Heimdall.config.sseEventName = "heimdall";
Heimdall.config.sseReconnectDelayMs = 250;
Heimdall.config.sseReconnectMaxDelayMs = 10000;
Heimdall.config.sseReconnectBackoffFactor = 2;
Heimdall.config.sseSweepIntervalMs = 5000;
Heimdall.config.ssePauseWhenHidden = false;Per-subscriber HTML attributes:
heimdall-sse="orders"
heimdall-sse-topic="orders"
heimdall-sse-target="#orders"
heimdall-sse-swap="beforeend"
heimdall-sse-event="order.updated"
heimdall-sse-disable="true"14. Programmatic Invocation
Use Heimdall.invoke when a browser script needs to run the same content-invocation request pipeline as a DOM trigger. It supports the same synchronization coordinator, an optional client timeout, and an external AbortSignal.
const controller = new AbortController();
const result = await Heimdall.invoke(
"orders.refresh",
{ page: 2 },
{
target: "#orders",
swap: "inner",
headers: { "X-Request-Source": "toolbar" },
sync: "replace",
syncGroup: "orders",
timeoutMs: 5000,
signal: controller.signal,
onSuccess: result => console.log(result.ms),
onError: result => console.warn(result.status)
});
// Expected cancellation resolves normally.
if (result.cancelled) {
console.log(result.cancelReason);
}15. Programmatic SSE
Use Heimdall.sse.connect when a script needs to subscribe without a declarative heimdall-sse attribute. The element anchors lifecycle validation for that subscription.
const subscription = Heimdall.sse.connect("orders", {
element: document.querySelector("#orders-panel"),
target: "#orders",
swap: "beforeend",
event: "order.updated"
});
subscription.close();
Heimdall.sse.disconnect("#orders-panel");
Heimdall.sse.disconnectAll();16. Request, Swap, and Runtime Events
Heimdall exposes a dependency-free integration surface through bubbling DOM CustomEvents. Request configuration is mutable, request-before and swap-before are cancellable with preventDefault(), and finally always fires. Declarative invocations dispatch from the triggering element; programmatic and SSE work remains observable on document.
Heimdall.config.debug = true;
document.addEventListener("heimdall:request-config", event => {
event.detail.headers["X-Correlation-ID"] = crypto.randomUUID();
event.detail.sync = "replace";
event.detail.syncGroup = "search";
});
document.addEventListener("heimdall:request-before", event => {
// event.preventDefault() cancels before fetch.
});
document.addEventListener("heimdall:request-after", event => {});
document.addEventListener("heimdall:request-finally", event => {});
document.addEventListener("heimdall:request-cancel", event => {});
document.addEventListener("heimdall:request-timeout", event => {});
document.addEventListener("heimdall:swap-before", event => {
// Mutable: target, fragment, and swap.
// event.preventDefault() skips this swap.
});
document.addEventListener("heimdall:swap-after", event => {});
// Existing invocation/directive events remain supported.
document.addEventListener("heimdall:before", e => {});
document.addEventListener("heimdall:after", e => {});
document.addEventListener("heimdall:error", e => {});
document.addEventListener("heimdall:abort", e => {});
document.addEventListener("heimdall:redirect", e => {});
document.addEventListener("heimdall:javascript-error", e => {});
// SSE connection lifecycle events remain supported.
document.addEventListener("heimdall:sse-open", e => {});
document.addEventListener("heimdall:sse-close", e => {});
document.addEventListener("heimdall:sse-message", e => {});
document.addEventListener("heimdall:sse-error", e => {});
document.addEventListener("heimdall:sse-pause", e => {});
document.addEventListener("heimdall:sse-resume", e => {});
document.addEventListener("heimdall:sse-reconnect-scheduled", e => {});
document.addEventListener("heimdall:sse-abort", e => {});
document.addEventListener("heimdall:sse-redirect", e => {});17. When Changes Take Effect
Most values are read at the moment of use. A few bootstrapping values need to be set early because observers and sweep timers are created once.
Read on each request:
endpoints, debug, oobEnabled, antiforgery,
requestHeaders, clientInfo, clientInfoMaxAgeMs,
requestSync, requestTimeoutMs
Read from each trigger event:
inputDebounceMs, hoverDelayMs,
scrollThresholdPx, scrollMinIntervalMs
Set before first visible boot:
visibleRootMargin, visibleThreshold
Set before ready for automatic DOM observing:
observeDom
Set before SSE starts for initial sweeper interval:
sseSweepIntervalMs
Read during SSE reconnect/pause behavior:
sseReconnectDelayMs, sseReconnectMaxDelayMs,
sseReconnectBackoffFactor, ssePauseWhenHidden18. Practical Defaults
For most apps, keep the default endpoints, keep observeDom on, keep OOB on, and tune only the user-experience timings that your UI actually needs.
Common application-level tweaks:
Heimdall.config.debug = builderEnvironmentIsDevelopment;
Heimdall.config.inputDebounceMs = 300;
Heimdall.config.hoverDelayMs = 250;
Heimdall.config.requestSync = "parallel";
Heimdall.config.requestTimeoutMs = 0;
Heimdall.config.ssePauseWhenHidden = true;19. Antiforgery and Client Context
Antiforgery is enabled by default on the server and client. A global server opt-out removes Heimdall's need for AddAntiforgery and UseAntiforgery; keep them if another endpoint still uses ASP.NET Core antiforgery. A per-invocation RequireAntiforgeryToken(false) exception is narrower: keep the shared infrastructure whenever any other Heimdall invocation or Bifrost still validates tokens. Keep the browser setting aligned and set it before the first invocation or SSE subscription. Client information is a separate opt-in and remains untrusted presentation context.
// Global opt-out: every Heimdall invocation and Bifrost.
builder.Services.AddHeimdall(options =>
options.EnableAntiforgery = false);
var app = builder.Build();
app.UseHeimdall();
// No AddAntiforgery() or UseAntiforgery()
// is required by Heimdall in this mode.
// Matching browser opt-out, before first use.
Heimdall.config.antiforgery = false;
// Optional browser context
Heimdall.config.clientInfo = true;
Heimdall.config.clientInfoMaxAgeMs = 60_000;20. Asynchronous Headers
requestHeaders can return an object, header pairs, Headers, null, or a promise of any of those. It runs for content invocations and Heimdall security-token requests. If it throws or rejects, Heimdall fails the request before fetch.
Heimdall.config.requestHeaders = async context => ({
Authorization: `Bearer ${await auth.getToken({
signal: context.signal
})}`
});
document.addEventListener("heimdall:unauthorized", event => {
event.preventDefault();
auth.openSignIn();
});21. Content-Invocation Rate Limiting
Heimdall content invocations support ASP.NET Core's native rate-limiter policies. Register the built-in limiter services and middleware, then apply EnableRateLimiting or DisableRateLimiting to an invocation method or declaring type. Method metadata overrides type metadata.
using Microsoft.AspNetCore.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("content-invocations", limiter =>
{
limiter.PermitLimit = 60;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
});
app.UseRateLimiter();
app.UseHeimdall();
[EnableRateLimiting("content-invocations")]
[ContentInvocation("profile.save")]
public static IHtmlContent Save(ProfilePayload payload)
=> Profile.Render(payload);
// A narrow opt-out:
[DisableRateLimiting]
[ContentInvocation("health.ping")]
public static IHtmlContent Ping()
=> Html.Span("ok");22. Focused References
The configuration surface is intentionally summarized here. Use the focused pages for the full browser-context schema, cache invalidation, request-header precedence, unauthorized behavior, security boundaries, and telemetry setup.
/client-requests client info + request hooks
/security antiforgery + CORS + authentication
/observability traces + metricsPlugin Installation and Execution Bounds
A browser plugin has its own installation options and explicit loading order. Those options belong to the plugin and are separate from Heimdall.config and server options.
Heimdall.pluginApiVersion
Heimdall.use(editorPlugin, { commitKey: "Enter" });
Heimdall.plugins.list();
Heimdall.plugins.remove("company.editor");
Heimdall.dispose(root);
Heimdall.config.pluginMountTimeoutMs = 10000;
Heimdall.config.pluginCommandTimeoutMs = 10000;
Heimdall.config.pluginCommandMaxArgsChars = 65536;
Heimdall.config.pluginCommandMaxCount = 256;
Heimdall.config.pluginSseQueueLimit = 128;Zero disables a mount/command timeout. SSE queue overflow reports sse-command-queue-full and closes the subscription with plugin-command-queue-full. NuGet or server assembly registration does not load browser assets. See the plugin guide for compatibility, startup ordering, async readiness, cancellation, and diagnostics.
Next Steps
After configuration, the most useful references are the raw HTML attribute surface, the runtime behavior model, and the security boundaries around response processing.