Skip to main content

Reference

Complete reference of the Provider SDK's public surface — types, HTTP endpoints, NATS subjects, CloudEvents catalogue, and database schema.

Registration

AddBeaconTowerProvider<TAdapter>

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

Registers all SDK services into the DI container (see Getting Started for the full registration order).

RegistrationLifetimeDescription
TAdapter / IDeviceAdapterSingletonYour protocol adapter
INatsConnection (keyed "btbus")SingletonCanonical-wire bus connection — host registers via AddBeaconTowerBusApi; the SDK resolves it as a dependency.
INatsConnectionManagerSingletonShared NATS connection (data plane: telemetry + events)
INatsJetStreamPublisherSingletonJetStream publish helper
ITemplateClientSingletonResolves templateId against core-management; default NatsTemplateClient (60 s TTL, 5 s call budget)
IWebhookTargetResolverSingletonRouting-target lookup (options-backed by default)
IHttpWebhookPublisherSingleton (HttpClientFactory)HMAC-signed webhook dispatch
IEventPublisherSingletonCloudEvents catalogue, dual-transport
IDesiredDeliveryTrackerSingletonDesired-property delivery tracking
IDeviceRepository, IDeviceTwinRepository, IDeviceStatusRepository, IConnectionLogRepository, ILogFilterRepositoryScopedPersistence
IMessageBufferScopedDiagnostic message ring buffer
IDeviceRegistryScopedDevice queries / validation
IProvisioningManagerScopedProvisioning orchestration
ICredentialsServiceScopedKey gen / encrypt / hash / re-encrypt
IProviderApiService, IDevicesApiService, ITwinManagementServiceScopedService-layer APIs (host wires HTTP)
TransactionHelperScopedTransactionScope wrapper
TelemetryPipelineSingleton + HostedServiceChannel-bounded ingest, JetStream publish
BusApiConsumersHostedServiceNATS request/reply handlers
DeviceStatusProjectionSingleton + HostedServiceConnection-status projection
ConnectionLogStoreSingleton + HostedServiceConnection-log batcher
LogFilterServiceSingleton + HostedServiceActive-filter polling
LogRetentionServiceHostedServiceConnection-log purge sweep
AdapterLifecycleServiceHostedServiceCalls adapter StartAsync / StopAsync
HardPruneBackgroundServiceHostedService (optional)Hard-prune of soft-deleted devices
AdapterContextSingletonWires TelemetryPipeline + DeviceStatusProjection to the adapter

Background services are skipped when ASPNETCORE_ENVIRONMENT == Test or Testing.

Adapter types

IDeviceAdapter

public interface IDeviceAdapter : IAsyncDisposable
{
AdapterCapabilities Capabilities { get; }
Task StartAsync(CancellationToken ct);
Task StopAsync(CancellationToken ct);
Task<ProvisionResult> OnProvisionAsync(ProvisionContext context, CancellationToken ct);
Task<ProvisionResult> OnUnprovisionAsync(string deviceId, CancellationToken ct);
Task SendDesiredPropertiesAsync(string deviceId, JsonObject properties, CancellationToken ct);
Task<CommandResult> SendCommandAsync(AdapterCommand command, CancellationToken ct);
bool IsDeviceConnected(string deviceId);
}

AdapterCapabilities

public sealed record AdapterCapabilities
{
public bool SupportsRetain { get; init; }
public bool RetainSurvivesRestart { get; init; }
public bool SupportsCommands { get; init; }
}

ProvisionContext / ProvisionResult

public record ProvisionContext
{
public required string DeviceId { get; init; }
public required DeviceAuthMethod AuthMethod { get; init; } // SymmetricKey | Certificate
public string? PrimaryKey { get; init; }
public string? SecondaryKey { get; init; }
public string? PrimaryKeyHash { get; init; }
public string? SecondaryKeyHash { get; init; }
}

public record ProvisionResult
{
public bool Success { get; init; }
public string? Error { get; init; }
public static ProvisionResult Ok();
public static ProvisionResult Failed(string error);
}

AdapterCommand / CommandResult / CommandStatus

public record AdapterCommand
{
public required string DeviceId { get; init; }
public required string MethodName { get; init; }
public JsonObject? Payload { get; init; }
public string? CorrelationId { get; init; }
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
}

public sealed record CommandResult
{
public required string CorrelationId { get; init; }
public required CommandStatus Status { get; init; }
public int? StatusCode { get; init; } // device-side, Ok/DeviceError only
public JsonObject? Response { get; init; } // device body, null when none
public string? ErrorCode { get; init; } // = nameof(Status) for non-Ok/non-DeviceError
public string? ErrorMessage { get; init; }
public required bool Retryable { get; init; }
}

public enum CommandStatus
{
Ok, DeviceError, DeviceNotConnected, MethodTimeout,
DuplicateCorrelationId, PendingCapacityExceeded,
PayloadTypeMismatch, TransportError, Cancelled
}

There are no Ok() / Failed() factories on CommandResult — adapters construct instances explicitly. See Adapter Interface › SendCommandAsync.

SDK clients (AdapterContext)

public interface ITelemetryClient
{
void Send(string deviceId, TelemetryMessage message);
}

public record TelemetryMessage
{
public required ReadOnlyMemory<byte> Payload { get; init; }
public bool IsReportedProperties { get; init; }
}

public interface IPropertiesClient
{
void Report(string deviceId, JsonObject properties);
}

public interface IDiagnosticsClient
{
void ReportConnected(string deviceId, ConnectionMetadata metadata);
void ReportDisconnected(string deviceId, string reason, ConnectionMetadata? metadata = null);
void ReportAuthFailure(string deviceId, string detail);
void ReportEvent(string deviceId, DiagnosticEvent evt);
}

TelemetryPipeline implements ITelemetryClient + IPropertiesClient. DeviceStatusProjection implements IDiagnosticsClient.

ITemplateClient

Resolves an opaque templateId (sent by onboarding on provision) to a provider template stored in core-management. The SDK calls ITemplateClient.GetAsync from the bus-api consumer's provision path before invoking your adapter's OnProvisionAsync.

public interface ITemplateClient
{
Task<ProviderTemplate> GetAsync(string templateId, CancellationToken ct);
}

public sealed record ProviderTemplate(
string TemplateId,
string ProviderType,
ReadOnlyMemory<byte> Body); // opaque MessagePack-encoded template

The default implementation, NatsTemplateClient, is registered by AddBeaconTowerProvider and resolves over the canonical wire:

  • Subject: BusApiSubjects.GetProviderTemplatebt.coremgmt.providerTemplates.get
  • Connection: keyed INatsConnection ("btbus") from AddBeaconTowerBusApi
  • Cache: positive + negative, 60 s TTL each
  • Timeout: 5 s application timeout, RequestNoResponders=true for fast-fail
  • Errors: maps to a closed code set (transport.{no-responders,timeout,error,malformed-reply} and template.not-found / template.{custom})

NatsTemplateClient is thread-safe and a singleton — share it across all provision concurrency. Replace it via Services.Replace(...) after AddBeaconTowerProvider for tests or out-of-band template stores. See Template Resolution for the full wire shape, cache semantics, and transport-fault mapping.

Canonical-wire reply envelope

All provider control-plane replies (provision, unprovision, get-credential, etc.) and the core-management GetProviderTemplate reply are wrapped in BusApiResult<TResponse> from BeaconTower.Bus.Contracts.Common. The discriminator means error replies cannot deserialize as malformed success bodies:

[MessagePackObject]
public sealed record BusApiResult<TResponse>(
[property: Key(0)] bool IsSuccess,
[property: Key(1)] TResponse? Response,
[property: Key(2)] ErrorResponse? Error);

[MessagePackObject]
public sealed record ErrorResponse(
[property: Key(0)] string Error, // machine code, snake_case
[property: Key(1)] string Message); // human description

Callers must inspect IsSuccess before reading Response — the SDK's internal helpers (NatsTemplateClient, the bus-api consumer) do this for you, but in-house bridges that proxy these subjects must as well.

The Initial* / Desired / Reported / Payload fields on contract records are byte[]? — opaque MessagePack-encoded blobs. Templates and twin payloads are intentionally schemaless from the platform's perspective; each tenant defines its own shape.

NATS bus subjects

Pattern: bt.provider.{providerId}.{category}.{operation}.{deviceId}. Every request carries schemaVersion: 1. Replies use the canonical BusApiResult<TResponse> envelope; errors surface as ErrorResponse { error, message } inside the failure branch.

All subjects are constructed via BeaconTower.Bus.Contracts.Common.BusApiSubjects — treat the literal strings in this table as illustrative; the constants are the contract.

SubjectOperationQueue group
bt.provider.{pid}.admin.provision.*Provisionbusapi
bt.provider.{pid}.admin.unprovision.*Unprovision (idempotent)busapi
bt.provider.{pid}.admin.set-state.*Enable / disablebusapi
bt.provider.{pid}.admin.revoke-certificate.*Revoke certificatebusapi
bt.provider.{pid}.admin.get-credential.*Get credentialsbusapi
bt.provider.{pid}.admin.device-exists.*Existence checkbusapi
bt.provider.{pid}.admin.get-device-data.*Get device databusapi
bt.provider.{pid}.cmd.desired.*Set desired (RFC 7396 merge-patch)busapi
bt.provider.{pid}.cmd.get-desired.*Get desiredbusapi
bt.provider.{pid}.cmd.get-reported.*Get reportedbusapi
bt.provider.{pid}.cmd.method.*Invoke method(broadcast — no queue group)

Method invocation is broadcast: every replica receives the request, only the one whose IsDeviceConnected(deviceId) returns true answers.

Outbound (provider → core-management)

The provider SDK consumes the following subject from core-management as part of templateId passthrough:

SubjectOperationCaller
bt.coremgmt.providerTemplates.getResolve provider template by ID (60 s cache, 5 s budget)NatsTemplateClient

Internal NATS broadcasts

Pattern: bt.{providerId}.internal.{kind}.{deviceId}. Empty payload, post-commit, best-effort. Recipients must invalidate caches and re-read state.

KindTrigger
registry-invalidateDevice row changed
state-changedEnabled/disabled flipped
desired-changedDesired property updated
cert-revokedCertificate revoked

HTTP endpoints

The SDK ships service-layer types (IDevicesApiService, IProviderApiService, ITwinManagementService); HTTP controllers are wired by the host. The reference MQTT host (BeaconTower.ProviderMqtt.Api) exposes the surface below — new providers either reuse or replicate this controller layer.

JWT Bearer required on every endpoint. The reference MQTT host serializes its REST controllers as snake_case for back-compat with existing consumers; new SDK-owned controllers should use camelCase.

Devices API (/api/devices)

MethodPathDescription
POST/api/devicesCreate device
GET/api/devicesList devices (paginated; soft-deleted excluded)
GET/api/devices/{deviceId}Get device
DELETE/api/devices/{deviceId}Soft-delete
GET/api/devices/{deviceId}/keysGet plaintext credentials (elevated authorization)
PATCH/api/devices/{deviceId}/stateToggle Enabled/Disabled

deviceId matches ^[a-zA-Z0-9_-]+$, max 128 chars. Uniqueness is a DB constraint, not advisory.

Twin management (/api/devices/{id}/...)

MethodPathDescription
GET/api/devices/{id}/twinGet full twin
GET/api/devices/{id}/desired?component={pointer}Get desired (optional RFC 6901 pointer filter)
GET/api/devices/{id}/reportedGet reported
PATCH/api/devices/{id}/desiredRFC 7396 JSON Merge Patch
PATCH/api/devices/{id}/tagsRFC 7396 JSON Merge Patch (cloud-only)

Updates apply atomically as read-merge-write, serialized per device. Versions start at 0 and increment per update. Reported writes are not accepted on these endpoints — reported state flows exclusively through the telemetry pipeline. Component-filter on a non-object value returns 404.

Provider API (/devices/{id}/...)

Cross-tenant service path consumed by onboarding. No /api prefix. OAuth2 client-credentials JWT.

MethodPathDescription
POST/devices/{id}/provisionProvision (idempotent on conflict, returns created: false)
GET/devices/{id}Get device
GET/devices/{id}/credentialsGet plaintext credentials
DELETE/devices/{id}Idempotent delete (HTTP 204 either way)
POST/devices/{id}/revoke-certificateRevoke certificate (HTTP 404 on missing device)

Reserved tag namespace _* requires provider.internal scope. Template field cap is 1 MB; nesting depth ≤ 10 (consistent with twin PATCH).

Diagnostics endpoints

All gated by Diagnostics:Enabled (HTTP 404 when disabled). JWT required.

MethodPathDescription
GET/api/diagnostics/infoRuntime snapshot — provider id, build version, tracked-device count
GET/api/diagnostics/devicesPer-device status, paginated
GET/api/diagnostics/devices/{id}Single-device status
GET/api/diagnostics/devices/summaryAggregate counts (cache TTL ≤ 30 s)
GET/api/diagnostics/devices/flappingDevices currently marked as flapping
GET/api/diagnostics/logs/{deviceId}Connection log for device (allow-list filter required)
POST / PUT / DELETE/api/diagnostics/filtersManage active log filters

Filters are an allow-list — no logs are stored without an active filter.

CloudEvents catalogue

CloudEvents 1.0 structured-mode JSON, dual-transport (NATS JetStream BEACONTOWER_EVENTS + HMAC-signed webhook). Catalogue is closed:

TypeTrigger
device.provisionedSuccessful provision
device.deletedSoft-delete (and on hard-prune)
device.state-changedEnabled ↔ Disabled (oldState / newState are lowercase enabled / disabled)
device.connectedAdapter ReportConnected
device.disconnectedAdapter ReportDisconnected
device.kickedAdapter forcibly disconnected device
device.certificate-revokedrevoke-certificate admin call

Envelope:

  • source = /providers/{providerId}
  • subject = deviceId
  • id = UUIDv7 (cross-transport deduplication)
  • time = RFC 3339 UTC
  • data is camelCase

Webhook headers: X-Webhook-Signature: sha256=<hex(HMAC)>, X-Webhook-Event-Id, X-Webhook-Event-Type.

Database schema

The SDK manages these tables via DbUp migrations:

TablePurpose
device_entityIdentity, encrypted credentials, state
device_twin_entityDesired/reported/tags, version counters
device_status_entityCurrent connection status per device
connection_log_entityConnection lifecycle events (immutable)
log_filter_entitySaved diagnostic log filters (allow-list)
message_bufferRing buffer for diagnostic message capture
schemaversionsDbUp migration journal

All entity tables use the BeaconTower.Data.PostgreSql convention: UUID primary key, JSONB document, soft-delete via deleted_at.

Configuration sections

SectionBound to
ProviderProviderSdkOptions (Action<ProviderSdkOptions> configurator)
CredentialsCredentialsOptions
NatsNatsOptions
Nats:EventsBeaconTower.Events publisher
TelemetryPipelineTelemetryPipelineOptions
BusApiBusApiOptions
WebhookWebhookOptions (multi-target)
DiagnosticsDiagnosticsOptions
ProvisioningProvisioningOptions
TransformerTransformerOptions
AuthenticationOptionshost-managed (JWT Bearer — MetadataAddress, Authority, Audience; canonical pattern from BeaconTower.Core.API)

See Configuration for option-level details and Operational Notes for the production gotchas.