Skip to main content

Template Resolution

When onboarding provisions a device, it sends only a templateId — never the template body. The provider resolves that ID to a concrete provider template on demand by calling core-management over the canonical NATS+MessagePack wire. This page covers the wire, the cache semantics, and how transport faults surface to the provisioning request.

Released in mqttprovider 1.38.x alongside the canonical-wire migration. Pre-1.38 builds embedded the template payload in the provision request; that path is removed.

Why passthrough

The provision request is the hot path: every device that boots into onboarding hits it. Sending the full template payload on every request meant:

  • Wire amplification — large templates ride along even when nothing changed, blowing out NATS message size budgets.
  • Cache inversion — onboarding became responsible for caching templates it doesn't own, with stale-window semantics that didn't match core-management's truth.
  • Authoring lag — template edits in core-management didn't take effect until onboarding's cache turned over.

templateId passthrough flips this: onboarding forwards an opaque identifier; the provider — which sits closest to the device and already has a NATS connection — resolves it against core-management. Templates are cached at the edge that actually consumes them.

Wire shape

onboarding ──provision(templateId)──▶ provider


ITemplateClient

┌───────────────┴────────────────┐
│ cache hit (≤ 60 s) │
│ → return cached template │
├────────────────────────────────┤
│ cache miss / negative-expired │
│ → NATS request (5 s budget) │
▼ ▼
cache and return BusApiResult.Failure
→ ProvisionResult.Failed

Subject

bt.coremgmt.providerTemplates.get

Built via BeaconTower.Bus.Contracts.Common.BusApiSubjects.GetProviderTemplate — treat the literal string as illustrative; the constant is the contract.

Request / response contracts

[MessagePackObject]
public sealed record GetProviderTemplateRequest(
[property: Key(0)] string TemplateId);

[MessagePackObject]
public sealed record GetProviderTemplateResponse(
[property: Key(0)] string TemplateId,
[property: Key(1)] string ProviderType,
[property: Key(2)] byte[] Body); // opaque MessagePack-encoded template

The reply is wrapped in BusApiResult<GetProviderTemplateResponse> (see Canonical wire reply envelope). 404 surfaces as BusApiResult.Failure with code template.not-found; treat this as a permanent classification (no retry — the template has not been authored).

Cache semantics — NatsTemplateClient

The default ITemplateClient implementation, NatsTemplateClient, is a process-wide singleton that ships with the SDK. It maintains two caches keyed by templateId:

CacheTTLTrigger
Positive60 sSuccessful resolve
Negative60 stemplate.not-found reply

Entries are evicted lazily on read. There is no background sweep — a stale-but-unused entry simply lingers until the next miss.

The TTL is intentionally short. Template authoring in core-management is rare but visible: a 60-second worst-case propagation matches operator expectations without forcing a synchronous round-trip on every provision.

Negative caching

Negative entries exist to absorb the common authoring-lag case: a tenant references a template ID that exists in their config but hasn't been published to core-management yet. Without negative caching, a misconfigured fleet would hammer the bus with hundreds of identical 404s per second.

If you need to invalidate a negative entry sooner than 60 s (e.g., the template has just been authored and you want a hot retry), restart the provider. There is no public flush API by design — caches must be safe under concurrent load and a flush hook is a foot-gun.

Timeouts and fast-fail

Each resolve call carries a 5 s application timeout and sets RequestNoResponders = true on the underlying NATS request. The combination gives you two distinct failure modes:

ConditionSurfaceRetry classification
No core-management consumer registeredImmediate NoRespondersTransient — operator action
Consumer registered, slow reply5 s timeoutTransient — retry next provision
Consumer registered, error replyBusApiResult.Failure with codeCode-dependent (see below)

RequestNoResponders=true is what stops the provider from sitting on a 5 s timeout when core-management is entirely down — you get a fast NoResponders and can fail the provision request with a clear transport.no-responders classification.

Transport-fault mapping

NatsTemplateClient maps NATS / Bus.Contracts errors onto a closed set of provider-side error codes. Adapters and provisioning callers should not invent their own codes — match these exactly:

SourceProvider-side codeRetry?
NatsNoRespondersExceptiontransport.no-respondersYes (transient)
Application timeout (5 s)transport.timeoutYes (transient)
Generic NatsExceptiontransport.errorYes (transient)
MessagePack deserialization failuretransport.malformed-replyNo (contract violation — log loudly)
BusApiResult.Failure { error = "template.not-found" }template.not-foundNo (permanent)
BusApiResult.Failure { error = "*" } (other)template.{error} (passthrough)Code-dependent

The retry policy is the caller's responsibility: IProvisioningManager classifies transport.* as transient and surfaces them as ProvisionResult.Failed("transport.*") with the bus-api consumer returning ProviderUnavailable. template.not-found is permanent and the device row is not created.

Replacing ITemplateClient

The SDK registers NatsTemplateClient as the default implementation. Tests and aggregator-side providers that resolve templates through a different control plane can replace it after AddBeaconTowerProvider:

builder.Services.AddBeaconTowerProvider<MyAdapter>(builder, options =>
{
options.ProviderId = "my-provider";
});

builder.Services.Replace(
ServiceDescriptor.Singleton<ITemplateClient, FakeTemplateClient>());

Implementations must:

  • Be thread-safe — the SDK calls in from BusApi consumer threads.
  • Honour the 5 s timeout budget — provision callers cannot extend it.
  • Return the same closed error-code set above so retry classification remains consistent across adapters.

Provisioning options

Two options under the Provisioning configuration section govern template resolution. They are documented in full at Configuration › Provisioning; the defaults are sized for the canonical-wire profile:

OptionDefaultEffect
TemplateCacheTtlSeconds60Positive + negative cache TTL. Lower it only if you accept the extra core-management traffic.
TemplateResolveTimeoutSeconds5Per-call application timeout. Must be ≤ the bus-api consumer's overall budget.

There is no separate negative-cache TTL knob — operationally the two windows are always equal so cache pressure stays predictable.