Getting Started
Build a minimal provider that accepts HTTP-posted telemetry and forwards it to Beacon Tower. The recipe below mirrors the reference BeaconTower.MqttProvider.Api/Program.cs in the mqttprovider repository.
Prerequisites
- .NET 10 SDK
- PostgreSQL (local or containerized)
- NATS Server with JetStream enabled
- A Beacon Tower tenant with an onboarding service (or the k3s dev setup)
BeaconTower.Bus.Contracts ≥ 1.1.0andBeaconTower.BusApion thebeacontower-azureAzure DevOps NuGet feed (transitive viaBeaconTower.ProviderSdk≥ 1.38.x; see your service'snuget.configfor the source mapping)
1. Create the project
dotnet new web -n BeaconTower.HttpProvider
cd BeaconTower.HttpProvider
dotnet add package BeaconTower.ProviderSdk
The SDK transitively depends on BeaconTower.Data.PostgreSql, BeaconTower.Events, and BeaconTower.Observability.AspNetCore.
2. Implement the adapter
Create HttpAdapter.cs:
using System.Text.Json.Nodes;
using BeaconTower.ProviderSdk.Adapters;
public sealed class HttpAdapter(AdapterContext context) : BaseAdapter(context)
{
public override AdapterCapabilities Capabilities { get; } = new()
{
SupportsRetain = false,
RetainSurvivesRestart = false,
SupportsCommands = false,
};
public override Task StartAsync(CancellationToken ct) => Task.CompletedTask;
public override Task StopAsync(CancellationToken ct) => Task.CompletedTask;
public override Task<ProvisionResult> OnProvisionAsync(
ProvisionContext context, CancellationToken ct)
{
// No protocol-specific setup needed for HTTP polling.
return Task.FromResult(ProvisionResult.Ok());
}
public override Task<ProvisionResult> OnUnprovisionAsync(
string deviceId, CancellationToken ct)
{
return Task.FromResult(ProvisionResult.Ok());
}
public override Task SendDesiredPropertiesAsync(
string deviceId, JsonObject properties, CancellationToken ct)
{
// HTTP devices poll for desired properties — nothing to push.
return Task.CompletedTask;
}
public override Task<CommandResult> SendCommandAsync(
AdapterCommand command, CancellationToken ct)
{
// HTTP devices don't support real-time commands.
return Task.FromResult(new CommandResult
{
CorrelationId = command.CorrelationId ?? string.Empty,
Status = CommandStatus.DeviceNotConnected,
ErrorCode = nameof(CommandStatus.DeviceNotConnected),
ErrorMessage = "HTTP devices do not support direct methods.",
Retryable = false,
});
}
public override bool IsDeviceConnected(string deviceId) => false;
/// <summary>
/// Called by the telemetry endpoint to forward bytes into the SDK pipeline.
/// </summary>
public void IngestTelemetry(string deviceId, ReadOnlyMemory<byte> payload)
{
Telemetry.Send(deviceId, new TelemetryMessage
{
Payload = payload,
IsReportedProperties = false,
});
}
}
Telemetry.Send is fire-and-forget; the SDK's batched pipeline owns delivery to NATS JetStream from there. Reported properties go through Properties.Report(deviceId, JsonObject) (or Telemetry.Send with IsReportedProperties = true when you already have bytes).
3. Wire up Program.cs
using BeaconTower.ProviderSdk.Extensions;
using BeaconTower.Observability.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
// Dapper snake_case ↔ PascalCase (AppDomain-global)
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
var builder = WebApplication.CreateBuilder(args);
// 1. .NET Aspire defaults — OpenTelemetry, health checks, HTTP resilience.
builder.AddServiceDefaults();
// 2. BusApi — keyed "btbus" NATS connection used by the canonical wire
// (provider control-plane RPC + ITemplateClient → core-management).
// MUST be registered before AddBeaconTowerProvider so the SDK can resolve
// [FromKeyedServices("btbus")] INatsConnection.
builder.Services.AddBeaconTowerBusApi(opts =>
{
opts.NatsUrl = builder.Configuration["BusApi:NatsUrl"];
opts.User = builder.Configuration["BusApi:User"];
opts.Password = builder.Configuration["BusApi:Password"];
// TLS knobs as applicable
});
// 3. Provider SDK — adapter, repositories, background services, PostgreSQL,
// DbUp migrations, CloudEvents publisher, NATS connection, ITemplateClient.
builder.Services.AddBeaconTowerProvider<HttpAdapter>(builder, options =>
{
options.ProviderId = builder.Configuration["Provider:Id"] ?? "http-provider";
});
// 4. Authentication — JWT in production, dev-bypass in Development.
// Production wiring matches the canonical pattern in BeaconTower.Core.API:
// config under AuthenticationOptions:*, OIDC discovery via MetadataAddress,
// ValidateIssuer = true as the only explicit validator (others rely on
// Microsoft's defaults, which are already true).
if (builder.Environment.IsDevelopment())
{
builder.Services.AddAuthentication("DevBypass")
.AddScheme<AuthenticationSchemeOptions, DevBypassAuthHandler>(
"DevBypass", _ => { });
}
else
{
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x =>
{
x.MetadataAddress = builder.Configuration["AuthenticationOptions:MetadataAddress"];
x.Authority = builder.Configuration["AuthenticationOptions:Authority"];
x.Audience = builder.Configuration["AuthenticationOptions:Audience"];
x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true };
});
}
builder.Services.AddAuthorization();
// 5. Observability metrics (required for [TrackMetrics] attribute).
builder.Services.AddBeaconTowerMetrics();
// 6. Controllers — your telemetry/admin endpoints.
builder.Services.AddControllers();
var app = builder.Build();
// 7. Diagnostics enable-toggle middleware MUST run before auth so the
// spec-mandated 404 happens regardless of token shape.
app.UseMiddleware<DiagnosticsEnabledMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapDefaultEndpoints();
app.MapMetrics();
await app.RunAsync();
public partial class Program;
If you have protocol bootstrap that must complete before RunAsync() (the MQTT reference host calls await app.Services.SeedMqttSuperuserAsync() here so go-auth finds the broker superuser row), insert it between builder.Build() and the middleware chain. See Operational Notes › Startup ordering.
AddBeaconTowerProvider<HttpAdapter> registers, in order:
- Adapter-contract defaults (transformer + resolver + circuit-breaker options).
- Options binding —
Credentials,Nats,TelemetryPipeline,Webhook,Diagnostics,BusApi,Provisioning. - Singletons — NATS connection, JetStream helper, the adapter, the webhook publisher (
HttpClientFactory-managed), the event publisher,ITemplateClient(default:NatsTemplateClientresolving over the keyedbtbusconnection). - Scoped services — repositories,
MessageBuffer,IProvisioningManager,ICredentialsService,IProviderApiService,IDevicesApiService,ITwinManagementService,TransactionHelper. - Singletons consumed by
AdapterContext—TelemetryPipeline,DeviceStatusProjection,LogFilterService,ConnectionLogStore,FlapDetector,DeviceStatusSummaryCache. - Background services (skipped when
ASPNETCORE_ENVIRONMENT == Test/Testing) —AdapterLifecycleService,TelemetryPipeline,BusApiConsumers,DeviceStatusProjection,LogRetentionService,LogFilterService,ConnectionLogStore, optionalHardPruneBackgroundService. AdapterContextfactory — wiresTelemetryPipelineas bothITelemetryClientandIPropertiesClient, andDeviceStatusProjectionasIDiagnosticsClient.- PostgreSQL via
BeaconTower.Data.PostgreSql, then DbUp migrations. - CloudEvents publisher (
BeaconTower.Events) bound toNats:Events:ServiceName.
BusApiConsumers resolves ITemplateClient on the provision path: when an
incoming bt.provider.{providerId}.admin.provision.* request carries a
TemplateId, the SDK calls ITemplateClient.GetAsync(templateId, ct) against
core-management before invoking your adapter's OnProvisionAsync. If the
template can't be resolved (template.not-found, transport timeout,
NoResponders), the bus-api consumer fails the provision with a structured
error — your adapter is not called. See Template Resolution
for the full wire shape and error-code map.
4. Add a telemetry endpoint
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/telemetry")]
public class TelemetryController : ControllerBase
{
private readonly HttpAdapter _adapter;
public TelemetryController(HttpAdapter adapter)
{
_adapter = adapter;
}
[HttpPost("{deviceId}")]
public IActionResult PostTelemetry(
[FromRoute] string deviceId,
[FromBody] byte[] payload)
{
_adapter.IngestTelemetry(deviceId, payload);
return Accepted();
}
}
The reference MQTT host's controllers (DevicesController, DiagnosticInfoController, etc.) live in the adapter rig (BeaconTower.ProviderMqtt) and are mounted via AddControllers().AddApplicationPart(typeof(DevicesController).Assembly). New providers either copy the controller surface or implement their own admin/REST endpoints.
5. Configure
appsettings.json:
{
"Provider": {
"Id": "http-provider"
},
"ConnectionStrings": {
"beacontower-mqttproviderdb": "Host=localhost;Database=http_provider;Username=postgres;Password=postgres"
},
"Nats": {
"Url": "nats://localhost:4222",
"Events": {
"ServiceName": "http-provider"
}
},
"BusApi": {
"NatsUrl": "nats://localhost:4222",
"User": "",
"Password": ""
},
"Credentials": {
"MasterKey": "<base64-32-byte-key>"
}
}
BusApi:* configures the keyed "btbus" INatsConnection registered by
AddBeaconTowerBusApi. In single-node dev it points at the same NATS server
as Nats:Url; in production they typically diverge (BusApi is the
canonical-wire control plane; Nats is the data plane for telemetry +
events). See Configuration › Bus API for the full
options block.
Generate a development master key:
openssl rand -base64 32
The default AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= (all zeros) is allowed in Development only — a production host without a real Credentials:MasterKey fails fast. See Configuration for the full options surface.
6. Run
dotnet run
The SDK will:
- Run DbUp migrations (devices, twins, status, connection logs, log filters).
- Connect to NATS and create the
TELEMETRYandBEACONTOWER_EVENTSJetStream streams. - Start the telemetry pipeline (channel-bounded ingest, batched JetStream publish).
- Start
BusApiConsumers(bt.provider.{providerId}.*request/reply). - Start your adapter via
AdapterLifecycleService.
Post telemetry:
curl -X POST http://localhost:5000/api/telemetry/my-device-001 \
-H "Content-Type: application/json" \
-d '{"temperature": 22.5, "humidity": 45}'
The SDK batches the message and publishes it to JetStream subject telemetry.http-provider::my-device-001 with headers messageType, providerId, providerClientId, enqueuedTime.
What the SDK handles automatically
| Feature | How it works |
|---|---|
| Provisioning | NATS bt.provider.{id}.admin.provision.* (canonical wire, MessagePack) — creates device + twin and calls your OnProvisionAsync inside the transaction. |
| Template resolution | ITemplateClient resolves templateId against core-management (bt.coremgmt.providerTemplates.get) on the keyed btbus connection; positive + negative cache TTL 60 s; 5 s call budget. See Template Resolution. |
| Credentials | AES-256-GCM at rest with versioned ciphertext, bcrypt hashing on a bounded threadpool, Credentials:PreviousMasterKeys for rotation, ICredentialsService.ReEncrypt(cipher) for bulk re-encrypt. |
| Twin state | Desired/reported with atomic versioning via JSONB; PATCH is RFC 7396 JSON Merge Patch; ?component= filter is RFC 6901 pointer. |
| Telemetry batching | Bounded channel (50,000 capacity) → 256-message / 50 ms batches → pipelined JetStream publish. |
| CloudEvents | device.provisioned, device.deleted, device.state-changed, device.connected, device.disconnected, device.kicked, device.certificate-revoked — published post-commit as CloudEvents 1.0 (NATS + HMAC-signed webhook in parallel). |
| Diagnostics | Per-device status, connection logs, flap detection (5 disconnects in 5 min → flapping, 10 min cooldown), /api/diagnostics/info for a snapshot. |
| Health checks | PostgreSQL health check registered automatically; /alive, /ready exposed by MapDefaultEndpoints. |
| Database schema | DbUp migrations run at startup (idempotent). |
Next steps
- Adapter Interface — Full
IDeviceAdaptercontract includingCapabilitiesand theCommandResultschema. - Telemetry Pipeline — Batching, backpressure, reported-property flow.
- Configuration — All available options.
- Desired Sync — Shadow hierarchy that exposes per-property convergence state without touching DTDL-shaped trees.
- Convergence Policies — Declarative retry / timeout / classification for desired→reported convergence (essential for REST aggregator adapters).
- Operational Notes — Production gotchas not in any single spec.
- Look at
BeaconTower.ProviderMqttin the mqttprovider repository for a production MQTT adapter implementation.