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).
| Registration | Lifetime | Description |
|---|---|---|
TAdapter / IDeviceAdapter | Singleton | Your protocol adapter |
INatsConnection (keyed "btbus") | Singleton | Canonical-wire bus connection — host registers via AddBeaconTowerBusApi; the SDK resolves it as a dependency. |
INatsConnectionManager | Singleton | Shared NATS connection (data plane: telemetry + events) |
INatsJetStreamPublisher | Singleton | JetStream publish helper |
ITemplateClient | Singleton | Resolves templateId against core-management; default NatsTemplateClient (60 s TTL, 5 s call budget) |
IWebhookTargetResolver | Singleton | Routing-target lookup (options-backed by default) |
IHttpWebhookPublisher | Singleton (HttpClientFactory) | HMAC-signed webhook dispatch |
IEventPublisher | Singleton | CloudEvents catalogue, dual-transport |
IDesiredDeliveryTracker | Singleton | Desired-property delivery tracking |
IDeviceRepository, IDeviceTwinRepository, IDeviceStatusRepository, IConnectionLogRepository, ILogFilterRepository | Scoped | Persistence |
IMessageBuffer | Scoped | Diagnostic message ring buffer |
IDeviceRegistry | Scoped | Device queries / validation |
IProvisioningManager | Scoped | Provisioning orchestration |
ICredentialsService | Scoped | Key gen / encrypt / hash / re-encrypt |
IProviderApiService, IDevicesApiService, ITwinManagementService | Scoped | Service-layer APIs (host wires HTTP) |
TransactionHelper | Scoped | TransactionScope wrapper |
TelemetryPipeline | Singleton + HostedService | Channel-bounded ingest, JetStream publish |
BusApiConsumers | HostedService | NATS request/reply handlers |
DeviceStatusProjection | Singleton + HostedService | Connection-status projection |
ConnectionLogStore | Singleton + HostedService | Connection-log batcher |
LogFilterService | Singleton + HostedService | Active-filter polling |
LogRetentionService | HostedService | Connection-log purge sweep |
AdapterLifecycleService | HostedService | Calls adapter StartAsync / StopAsync |
HardPruneBackgroundService | HostedService (optional) | Hard-prune of soft-deleted devices |
AdapterContext | Singleton | Wires 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.GetProviderTemplate→bt.coremgmt.providerTemplates.get - Connection: keyed
INatsConnection("btbus") fromAddBeaconTowerBusApi - Cache: positive + negative, 60 s TTL each
- Timeout: 5 s application timeout,
RequestNoResponders=truefor fast-fail - Errors: maps to a closed code set (
transport.{no-responders,timeout,error,malformed-reply}andtemplate.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.
| Subject | Operation | Queue group |
|---|---|---|
bt.provider.{pid}.admin.provision.* | Provision | busapi |
bt.provider.{pid}.admin.unprovision.* | Unprovision (idempotent) | busapi |
bt.provider.{pid}.admin.set-state.* | Enable / disable | busapi |
bt.provider.{pid}.admin.revoke-certificate.* | Revoke certificate | busapi |
bt.provider.{pid}.admin.get-credential.* | Get credentials | busapi |
bt.provider.{pid}.admin.device-exists.* | Existence check | busapi |
bt.provider.{pid}.admin.get-device-data.* | Get device data | busapi |
bt.provider.{pid}.cmd.desired.* | Set desired (RFC 7396 merge-patch) | busapi |
bt.provider.{pid}.cmd.get-desired.* | Get desired | busapi |
bt.provider.{pid}.cmd.get-reported.* | Get reported | busapi |
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:
| Subject | Operation | Caller |
|---|---|---|
bt.coremgmt.providerTemplates.get | Resolve 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.
| Kind | Trigger |
|---|---|
registry-invalidate | Device row changed |
state-changed | Enabled/disabled flipped |
desired-changed | Desired property updated |
cert-revoked | Certificate 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)
| Method | Path | Description |
|---|---|---|
POST | /api/devices | Create device |
GET | /api/devices | List devices (paginated; soft-deleted excluded) |
GET | /api/devices/{deviceId} | Get device |
DELETE | /api/devices/{deviceId} | Soft-delete |
GET | /api/devices/{deviceId}/keys | Get plaintext credentials (elevated authorization) |
PATCH | /api/devices/{deviceId}/state | Toggle Enabled/Disabled |
deviceId matches ^[a-zA-Z0-9_-]+$, max 128 chars. Uniqueness is a DB constraint, not advisory.
Twin management (/api/devices/{id}/...)
| Method | Path | Description |
|---|---|---|
GET | /api/devices/{id}/twin | Get full twin |
GET | /api/devices/{id}/desired?component={pointer} | Get desired (optional RFC 6901 pointer filter) |
GET | /api/devices/{id}/reported | Get reported |
PATCH | /api/devices/{id}/desired | RFC 7396 JSON Merge Patch |
PATCH | /api/devices/{id}/tags | RFC 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.
| Method | Path | Description |
|---|---|---|
POST | /devices/{id}/provision | Provision (idempotent on conflict, returns created: false) |
GET | /devices/{id} | Get device |
GET | /devices/{id}/credentials | Get plaintext credentials |
DELETE | /devices/{id} | Idempotent delete (HTTP 204 either way) |
POST | /devices/{id}/revoke-certificate | Revoke 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.
| Method | Path | Description |
|---|---|---|
GET | /api/diagnostics/info | Runtime snapshot — provider id, build version, tracked-device count |
GET | /api/diagnostics/devices | Per-device status, paginated |
GET | /api/diagnostics/devices/{id} | Single-device status |
GET | /api/diagnostics/devices/summary | Aggregate counts (cache TTL ≤ 30 s) |
GET | /api/diagnostics/devices/flapping | Devices currently marked as flapping |
GET | /api/diagnostics/logs/{deviceId} | Connection log for device (allow-list filter required) |
POST / PUT / DELETE | /api/diagnostics/filters | Manage 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:
| Type | Trigger |
|---|---|
device.provisioned | Successful provision |
device.deleted | Soft-delete (and on hard-prune) |
device.state-changed | Enabled ↔ Disabled (oldState / newState are lowercase enabled / disabled) |
device.connected | Adapter ReportConnected |
device.disconnected | Adapter ReportDisconnected |
device.kicked | Adapter forcibly disconnected device |
device.certificate-revoked | revoke-certificate admin call |
Envelope:
source = /providers/{providerId}subject = deviceIdid = UUIDv7(cross-transport deduplication)time = RFC 3339 UTCdatais 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:
| Table | Purpose |
|---|---|
device_entity | Identity, encrypted credentials, state |
device_twin_entity | Desired/reported/tags, version counters |
device_status_entity | Current connection status per device |
connection_log_entity | Connection lifecycle events (immutable) |
log_filter_entity | Saved diagnostic log filters (allow-list) |
message_buffer | Ring buffer for diagnostic message capture |
schemaversions | DbUp migration journal |
All entity tables use the BeaconTower.Data.PostgreSql convention: UUID primary key, JSONB document, soft-delete via deleted_at.
Configuration sections
| Section | Bound to |
|---|---|
Provider | ProviderSdkOptions (Action<ProviderSdkOptions> configurator) |
Credentials | CredentialsOptions |
Nats | NatsOptions |
Nats:Events | BeaconTower.Events publisher |
TelemetryPipeline | TelemetryPipelineOptions |
BusApi | BusApiOptions |
Webhook | WebhookOptions (multi-target) |
Diagnostics | DiagnosticsOptions |
Provisioning | ProvisioningOptions |
Transformer | TransformerOptions |
AuthenticationOptions | host-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.