Skip to main content

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.0 and BeaconTower.BusApi on the beacontower-azure Azure DevOps NuGet feed (transitive via BeaconTower.ProviderSdk ≥ 1.38.x; see your service's nuget.config for 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:

  1. Adapter-contract defaults (transformer + resolver + circuit-breaker options).
  2. Options binding — Credentials, Nats, TelemetryPipeline, Webhook, Diagnostics, BusApi, Provisioning.
  3. Singletons — NATS connection, JetStream helper, the adapter, the webhook publisher (HttpClientFactory-managed), the event publisher, ITemplateClient (default: NatsTemplateClient resolving over the keyed btbus connection).
  4. Scoped services — repositories, MessageBuffer, IProvisioningManager, ICredentialsService, IProviderApiService, IDevicesApiService, ITwinManagementService, TransactionHelper.
  5. Singletons consumed by AdapterContextTelemetryPipeline, DeviceStatusProjection, LogFilterService, ConnectionLogStore, FlapDetector, DeviceStatusSummaryCache.
  6. Background services (skipped when ASPNETCORE_ENVIRONMENT == Test/Testing) — AdapterLifecycleService, TelemetryPipeline, BusApiConsumers, DeviceStatusProjection, LogRetentionService, LogFilterService, ConnectionLogStore, optional HardPruneBackgroundService.
  7. AdapterContext factory — wires TelemetryPipeline as both ITelemetryClient and IPropertiesClient, and DeviceStatusProjection as IDiagnosticsClient.
  8. PostgreSQL via BeaconTower.Data.PostgreSql, then DbUp migrations.
  9. CloudEvents publisher (BeaconTower.Events) bound to Nats: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:

  1. Run DbUp migrations (devices, twins, status, connection logs, log filters).
  2. Connect to NATS and create the TELEMETRY and BEACONTOWER_EVENTS JetStream streams.
  3. Start the telemetry pipeline (channel-bounded ingest, batched JetStream publish).
  4. Start BusApiConsumers (bt.provider.{providerId}.* request/reply).
  5. 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

FeatureHow it works
ProvisioningNATS bt.provider.{id}.admin.provision.* (canonical wire, MessagePack) — creates device + twin and calls your OnProvisionAsync inside the transaction.
Template resolutionITemplateClient 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.
CredentialsAES-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 stateDesired/reported with atomic versioning via JSONB; PATCH is RFC 7396 JSON Merge Patch; ?component= filter is RFC 6901 pointer.
Telemetry batchingBounded channel (50,000 capacity) → 256-message / 50 ms batches → pipelined JetStream publish.
CloudEventsdevice.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).
DiagnosticsPer-device status, connection logs, flap detection (5 disconnects in 5 min → flapping, 10 min cooldown), /api/diagnostics/info for a snapshot.
Health checksPostgreSQL health check registered automatically; /alive, /ready exposed by MapDefaultEndpoints.
Database schemaDbUp migrations run at startup (idempotent).

Next steps

  • Adapter Interface — Full IDeviceAdapter contract including Capabilities and the CommandResult schema.
  • 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.ProviderMqtt in the mqttprovider repository for a production MQTT adapter implementation.