Getting Started with Granit.IoT
This walk-through wires Granit.IoT into an existing Granit host application and gets a real device → webhook → telemetry table flow running in five minutes. You’ll register the bundle, provision a device, point Scaleway IoT Hub at the webhook, and query the telemetry that lands in PostgreSQL.
What you’ll build
Section titled “What you’ll build”By the end of this page, your ASP.NET Core app exposes:
GET|POST|PUT|DELETE /iot/devices— device CRUDGET /iot/telemetry/{deviceId}— time-range telemetry queriesPOST /iot/ingest/scaleway— HMAC-validated webhook for Scaleway IoT Hub- Three recurring background jobs (purge, heartbeat, partition maintenance)
- Threshold alerts via
Granit.Notifications - Device lifecycle entries in
Granit.Timeline
sequenceDiagram
participant U as User / integrator
participant A as Your ASP.NET app
participant DB as PostgreSQL (iot_*)
participant S as Scaleway IoT Hub
participant D as Physical device
U->>A: POST /iot/devices
A->>DB: INSERT Device (Provisioning)
U->>A: PUT /iot/devices/{id}/activate
A->>DB: UPDATE Status = Active
D->>S: Publish MQTT message
S->>A: POST /iot/ingest/scaleway (HMAC-signed)
A->>DB: INSERT TelemetryPoint (JSONB metrics)
U->>A: GET /iot/telemetry/{id}?metric=temperature
A-->>U: TelemetryPointResponse[]
Prerequisites
Section titled “Prerequisites”- .NET 10 SDK
- PostgreSQL 16+ — local or managed (Scaleway DB, RDS, OVH)
- Redis 7+ — for transport-level deduplication
- An existing Granit host (see Project templates)
- Read access to the Granit GitHub Packages NuGet feed
-
Configure the Granit NuGet feed
Section titled “Configure the Granit NuGet feed”Granit.IoT packages live on GitHub Packages. Authenticate once with a personal access token (PAT) that has the
read:packagesscope. If the feed isn’t registered yet, add it first; otherwise update the credentials on the existing entry:Terminal window # First-time setupdotnet nuget add source https://nuget.pkg.github.com/granit-fx/index.json \--name granit-registry \--username YOUR_GITHUB_USERNAME \--password YOUR_GITHUB_PAT \--store-password-in-clear-text \--configfile nuget.config# Subsequent credential rotationdotnet nuget update source granit-registry \--username YOUR_GITHUB_USERNAME \--password YOUR_GITHUB_PAT \--store-password-in-clear-text \--configfile nuget.config -
Install the bundle
Section titled “Install the bundle”A single NuGet reference pulls in the full cloud-agnostic stack:
Terminal window dotnet add package Granit.Bundle.IoTThat’s 12 packages — domain, EF Core persistence, PostgreSQL migrations, device CRUD endpoints, ingestion pipeline, ingestion webhook, Scaleway provider, Wolverine handlers, background jobs, notifications bridge, timeline bridge, and the MCP bridge.
-
Register the modules
Section titled “Register the modules”Chain
.AddIoT()onto the existing Granit builder:using Granit.Bundle.IoT;var builder = WebApplication.CreateBuilder(args);builder.Services.AddGranit(builder.Configuration).AddIoT();var app = builder.Build();app.MapGranitIoTEndpoints(); // /iot/devices + /iot/telemetryapp.MapGranitIoTIngestionEndpoints(); // /iot/ingest/{source}app.Run();AddIoT()enumerates the 11 active modules in topological order (Granit.IoT.EntityFrameworkCore.Postgresships as migration helpers only, so it has no module class to register). Actual DI order is driven by each module’s[DependsOn(...)]graph — you can’t accidentally skip a dependency. -
Configure PostgreSQL, Redis, and Scaleway
Section titled “Configure PostgreSQL, Redis, and Scaleway”Add the following to
appsettings.json(placeholders only — never commit real secrets):{"ConnectionStrings": {"Default": "Host=localhost;Database=granit;Username=granit;Password=__FROM_SECRET_STORE__","Redis": "localhost:6379"},"IoT": {"TelemetryRetentionDays": 365,"HeartbeatTimeoutMinutes": 15,"Ingestion": {"Scaleway": {"SharedSecret": "__FROM_SECRET_STORE__","TopicDeviceSegmentIndex": 1}}},"RateLimiting": {"Policies": {"iot-ingest": { "PermitLimit": 100, "Window": "00:00:01" }}}}Key Default Purpose IoT:TelemetryRetentionDays365Per-tenant retention window enforced by StaleTelemetryPurgeJobIoT:HeartbeatTimeoutMinutes15Time without heartbeat before a device is flagged offline IoT:Ingestion:Scaleway:SharedSecret(required) HMAC secret configured in Scaleway IoT Hub IoT:Ingestion:Scaleway:TopicDeviceSegmentIndex1Index of the topic segment carrying the device serial -
Apply migrations
Section titled “Apply migrations”Granit.IoT ships its own isolated
IoTDbContext. Tables are prefixediot_and never share a context with another module:Terminal window dotnet ef database update \--context IoTDbContext \--project MyApp.HostThe initial migration creates the schema, including:
UNIQUE (tenant_id, serial_number)oniot_devices- BRIN index on
iot_telemetry_points.recorded_at(10× smaller than B-tree) - GIN index on
iot_telemetry_points.metrics(per-key JSONB queries) - Covering index
(device_id, recorded_at DESC)for the most common query
For high-volume deployments, enable monthly partitioning before your first production insert — see Operations. For the full schema (column types, secrets-at-rest, sizing per row count, AWS-companion tables), see Data model.
-
Provision your first device
Section titled “Provision your first device”Terminal window curl -X POST https://your-app/iot/devices \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"serialNumber": "ACME-TH-001","hardwareModel": "ACME-ThermoHumidity-v2","firmwareVersion": "1.4.2","label": "Warehouse A — aisle 3"}'Response —
201 Created:{"id": "7f3c9b2a-1d54-4a7e-9c1f-d3a8e5b0f9aa","serialNumber": "ACME-TH-001","status": "Provisioning","lastHeartbeatAt": null,"createdAt": "2026-05-20T08:12:33Z"}The device starts in
Provisioning. CallPUT /iot/devices/{id}/activateto move it toActive. Every transition is mirrored toGranit.Timelineautomatically. -
Point Scaleway IoT Hub at your webhook
Section titled “Point Scaleway IoT Hub at your webhook”In the Scaleway console:
- Open your IoT Hub and create a new Route of type REST.
- Endpoint URL:
https://your-app.example.com/iot/ingest/scaleway - HTTP verb:
POST. - Headers: let Scaleway add
X-Scaleway-Signatureautomatically. - Shared secret: same value as
IoT:Ingestion:Scaleway:SharedSecret. - Topic filter: e.g.
devices/+/telemetry— matches the configuredTopicDeviceSegmentIndexrule.
The first device message arriving on that topic hits your webhook, verifies HMAC-SHA256, deduplicates via Redis, enqueues a
TelemetryIngestedEtoon the Wolverine outbox, and returns202 Accepted. End-to-end latency from device publish to202stays under one second at P99. -
Query telemetry
Section titled “Query telemetry”Terminal window curl "https://your-app/iot/telemetry/$DEVICE_ID?from=2026-05-20T00:00:00Z&to=2026-05-20T23:59:59Z&maxPoints=500" \-H "Authorization: Bearer $TOKEN"Terminal window curl "https://your-app/iot/telemetry/$DEVICE_ID/latest" \-H "Authorization: Bearer $TOKEN"Terminal window curl "https://your-app/iot/telemetry/$DEVICE_ID/aggregate?metric=temperature&aggregation=avg&from=2026-05-20T00:00:00Z&to=2026-05-20T23:59:59Z" \-H "Authorization: Bearer $TOKEN"All telemetry endpoints are read-only and delegate to
ITelemetryReader. Multi-tenancy is enforced by a named query filter onTenantId, so a compromised JWT can never surface another tenant’s data.
What’s next
Section titled “What’s next”| I want to… | Read |
|---|---|
| Understand the domain model and state machine | Device management |
| See the full PostgreSQL schema, JSONB conventions, and sizing | Data model |
| Dive into the ingestion pipeline internals | Telemetry ingestion |
| Connect a non-Scaleway broker (Mosquitto, EMQX, HiveMQ, AWS IoT Core via MQTT) | MQTT transport |
| Pick a time-series backend (PostgreSQL native vs TimescaleDB) | Time-series storage |
| Prepare for production scale (partitioning, purge, heartbeat) | Operations |
| Provision AWS IoT Things with X.509 certs and Secrets Manager | AWS bridge |
| Route threshold alerts into my notification pipeline | Notifications bridge |
| Surface device lifecycle in my audit UI | Timeline bridge |
| Let Claude or Copilot talk to my fleet | MCP bridge |
| See the complete package list and bundle semantics | Bundle reference |