Skip to content

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.

By the end of this page, your ASP.NET Core app exposes:

  • GET|POST|PUT|DELETE /iot/devices — device CRUD
  • GET /iot/telemetry/{deviceId} — time-range telemetry queries
  • POST /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[]
  1. Granit.IoT packages live on GitHub Packages. Authenticate once with a personal access token (PAT) that has the read:packages scope. If the feed isn’t registered yet, add it first; otherwise update the credentials on the existing entry:

    Terminal window
    # First-time setup
    dotnet 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 rotation
    dotnet nuget update source granit-registry \
    --username YOUR_GITHUB_USERNAME \
    --password YOUR_GITHUB_PAT \
    --store-password-in-clear-text \
    --configfile nuget.config
  2. A single NuGet reference pulls in the full cloud-agnostic stack:

    Terminal window
    dotnet add package Granit.Bundle.IoT

    That’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.

  3. 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/telemetry
    app.MapGranitIoTIngestionEndpoints(); // /iot/ingest/{source}
    app.Run();

    AddIoT() enumerates the 11 active modules in topological order (Granit.IoT.EntityFrameworkCore.Postgres ships 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.

  4. 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" }
    }
    }
    }
    KeyDefaultPurpose
    IoT:TelemetryRetentionDays365Per-tenant retention window enforced by StaleTelemetryPurgeJob
    IoT: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
  5. Granit.IoT ships its own isolated IoTDbContext. Tables are prefixed iot_ and never share a context with another module:

    Terminal window
    dotnet ef database update \
    --context IoTDbContext \
    --project MyApp.Host

    The initial migration creates the schema, including:

    • UNIQUE (tenant_id, serial_number) on iot_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.

  6. 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. Call PUT /iot/devices/{id}/activate to move it to Active. Every transition is mirrored to Granit.Timeline automatically.

  7. In the Scaleway console:

    1. Open your IoT Hub and create a new Route of type REST.
    2. Endpoint URL: https://your-app.example.com/iot/ingest/scaleway
    3. HTTP verb: POST.
    4. Headers: let Scaleway add X-Scaleway-Signature automatically.
    5. Shared secret: same value as IoT:Ingestion:Scaleway:SharedSecret.
    6. Topic filter: e.g. devices/+/telemetry — matches the configured TopicDeviceSegmentIndex rule.

    The first device message arriving on that topic hits your webhook, verifies HMAC-SHA256, deduplicates via Redis, enqueues a TelemetryIngestedEto on the Wolverine outbox, and returns 202 Accepted. End-to-end latency from device publish to 202 stays under one second at P99.

  8. 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"

    All telemetry endpoints are read-only and delegate to ITelemetryReader. Multi-tenancy is enforced by a named query filter on TenantId, so a compromised JWT can never surface another tenant’s data.

I want to…Read
Understand the domain model and state machineDevice management
See the full PostgreSQL schema, JSONB conventions, and sizingData model
Dive into the ingestion pipeline internalsTelemetry 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 ManagerAWS bridge
Route threshold alerts into my notification pipelineNotifications bridge
Surface device lifecycle in my audit UITimeline bridge
Let Claude or Copilot talk to my fleetMCP bridge
See the complete package list and bundle semanticsBundle reference