This document specifies **STA Server**, a clean-room implementation of the [[OGC-STA-1|OGC SensorThings API Part 1: Sensing v1.1]] packaged as a Laravel 12 Composer library, together with two normative extensions adopted for the [[GEONOVUM-TESTBED|Geonovum Testbed Sensordata 2025]]: a **Device-Management extension** (the *RQ3* / OpenCitySense data model — DeviceModels, Configurations, DeviceSecrets and the controlling-connector relationship) and a **minimal Tasking subset** of [[OGC-STA-2]]. It defines the implementation profile of the SensorThings core that a conforming deployment exposes, the data model and HTTP interface of the extensions, the task notification mechanism over MQTT, the encrypted-secret handling, and the authentication and authorization model. It is the normative contract against which the STA Connector and the STA Portal are built.
This is a living technical specification maintained in the `sta-server` repository. It tracks the implemented behaviour of the package on its default branch; the source code is the ultimate reference, and this document is updated as part of the same change set whenever normative behaviour changes. Feedback and change proposals are handled through GitHub issues and pull requests on the repository. There is no external standards body governing this document; the SensorThings core behaviour it profiles is governed by the OGC.
## Introduction The Geonovum *Testbed Sensordata 2025* investigates how municipalities and water authorities can manage sensor data through open standards. The canonical open-source reference implementation of the SensorThings API is Fraunhofer IOSB's [[FROST|FROST-Server]] — a mature, widely deployed server that this project gratefully acknowledges as the benchmark for correct STA behaviour. FROST-Server is, however, licensed under LGPL-v3, which does not combine cleanly with the rest of the Collaborall platform. STA Server is therefore an independent implementation written directly against the [[OGC-STA-1]] specification on Laravel and PostgreSQL, re-licensed under Apache 2.0. [[FROST]] is consulted only at runtime as a behavioural oracle when the specification is ambiguous; no FROST source — Java, SQL, fixtures or error strings — was copied. On top of the SensorThings core, the testbed's third research question (*RQ3 — Device Registration*) requires a device-onboarding model. STA Server implements the [[OPENCITYSENSE]] data model one-to-one in naming so that future interoperability with FROST-PLUS deployments stays straightforward, while the migrations, models and controllers are fresh code. ### Scope This specification is normative for: - the **profile** of [[OGC-STA-1]] that a conforming server exposes (which entity sets, query options and conventions are supported, and where the profile constrains the base standard); - the **Device-Management extension** (DeviceModels, Configurations, DeviceSecrets, the `type` and `controlling_connector` properties on Things); - the **Tasking subset** (TaskingCapability and Task) and the MQTT task-notification mechanism; - the **authentication and authorization** model required of a host application; and - the **management interface** (health, statistics, server configuration, conformance runner and connector proxy). ### Out of scope The full normative text of the SensorThings core is **not** reproduced here; for the core behaviour of Things, Locations, Sensors, ObservedProperties, Datastreams, Observations, FeaturesOfInterest and HistoricalLocations, [[OGC-STA-1]] is authoritative and this document records only deviations and constraints. The following are explicitly out of scope for this revision: the OGC Tasking `Actuator` entity, SWE Common DataRecord encoding of tasking parameters, the STAplus extension, a `Decoder` entity and decoder runtime (a `decoder_ref` string is reserved instead), and any LoRaWAN network-server integration. ### Audience Implementers of clients against this server — in particular the STA Connector and the STA Portal — and operators deploying the package into a Laravel host.
As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative. The key words MAY, MUST, MUST NOT, RECOMMENDED, SHOULD, and SHOULD NOT in this document are to be interpreted as described in [[RFC2119]] [[RFC8174]] when, and only when, they appear in all capitals, as shown here. This specification defines the following independent **conformance classes**. A deployment MAY conform to any subset, but MUST advertise only the classes it fully implements. - **Core Sensing Profile** — the subset of [[OGC-STA-1]] defined in . A server claiming this class MUST satisfy every requirement in that section. - **Device-Management Extension** — the RQ3 data model and interface defined in . Conformance to this class REQUIRES conformance to the Core Sensing Profile. - **Tasking Subset** — the TaskingCapability/Task model and MQTT notification defined in . Conformance to this class REQUIRES conformance to the Device-Management Extension. - **Management Interface** — the operational endpoints defined in . This class is independent of the extension classes but REQUIRES the Core Sensing Profile. A **conforming server** is a deployment that satisfies the Core Sensing Profile and correctly advertises its conformance classes. A **conforming client** is software that interacts with a conforming server using only behaviour defined or referenced by this specification.
## Terminology This section defines the terms used throughout this document. Terms from [[OGC-STA-1]] (Thing, Location, Sensor, ObservedProperty, Datastream, Observation, FeatureOfInterest, HistoricalLocation) retain their meaning from that specification. - host application: the Laravel 12 application into which the STA Server package is installed. It provides the database connection, the authentication middleware and the authorization gate abilities. - connector: an on-premises agent, modelled as a [=Thing=] with `type` equal to `connector`, that ingests observations and executes [=task=]s on behalf of devices it controls. Specified separately in the *STA Connector — Technical Specification*. - device: a [=Thing=] with `type` equal to `device` that represents a managed sensor asset and MAY be linked to a controlling [=connector=]. - Thing: a SensorThings Thing as defined in [[OGC-STA-1]], extended by this specification with a `type` discriminator and a controlling-connector relationship. - device model: a reusable template describing a class of [=device=]s, including the sensors it carries and the JSON schema of its configuration. - configuration: a versioned record of the operating parameters applied to a [=device=], validated against its [=device model=]'s configuration schema. - device secret: a credential belonging to a [=device=], stored only as ciphertext sealed to a [=connector=]'s public key and never decryptable by the server. - task: a unit of work submitted to a [=connector=] through a [=tasking capability=], carrying an `action` and a target. - tasking capability: a declaration, attached to a [=connector=] [=Thing=], of the [=task=] actions it accepts.
## Architecture overview STA Server is distributed as the Composer package `collaborall/testbed-sensordata-server`. It is **host-agnostic**: it plugs into any Laravel 12 application that provides a dedicated PostgreSQL connection (`pgsql_sta`), an authentication middleware, and two authorization gate abilities (`sta.read`, `sta.write`). The package never touches the host's primary database. ### Runtime stack A conforming server runs on **PHP 8.4+** and **Laravel 12**, backed by **PostgreSQL 16+** with the **TimescaleDB** and **PostGIS** extensions on the dedicated `pgsql_sta` connection. The `observations` table is a TimescaleDB hypertable; geometry-valued columns use PostGIS. These storage choices are implementation detail and are not observable through the interface, except where this specification states a behavioural consequence (e.g. ordering and pagination semantics on the high-volume Observations set). ### Layering Requests enter through a single route group (`routes/sta.php`) registered under the configured prefix (`v1.1` by default). The pipeline is: host auth middleware → CORS → controller → OData query planner → Eloquent entity → PostgreSQL. JSON serialization is performed by resource classes that emit the OGC control information (`@iot.id`, `@iot.selfLink`, navigation links) and apply `$expand`/`$select` projection. ### Component context The server sits between the STA Portal (browser/admin client), the STA Connector (on-premises ingest and tasking agent) and an MQTT broker used to notify connectors of new tasks. The portal never speaks to the connector directly; it reads a connector *through* the server's connector proxy () so the connector's admin token stays server-side.
## Core Sensing Profile {#core-profile} This section defines the profile of [[OGC-STA-1]] that a server in the **Core Sensing Profile** conformance class exposes. For all behaviour not constrained here, [[OGC-STA-1]] is normative. ### Service root and entity sets A conforming server MUST expose the SensorThings service-root capabilities document at the configured prefix (`GET /v1.1`), listing the entity-set URLs. The server MUST implement the following eight core entity sets: `Things`, `Locations`, `HistoricalLocations`, `Sensors`, `ObservedProperties`, `Datastreams`, `Observations` and `FeaturesOfInterest`. The capabilities document MUST list at least the eight core entity sets. It MAY omit extension entity sets (`DeviceModels`, `Configurations`, `DeviceSecrets`, `TaskingCapabilities`, `Tasks`) from the `value` array even when the corresponding extension classes are implemented; clients MUST NOT treat the absence of an extension entity set from the capabilities document as evidence that the extension is unavailable, and SHOULD detect extension support by probing the entity set. ### Resource addressing The server MUST use the OGC parentheses grammar for entity addressing — `Things(1)`, not `Things/1`. Identifiers are 64-bit integers. Nested navigation paths (`Things(1)/Datastreams`, `Datastreams(1)/Observations`, and the inverse singletons) MUST be supported for every relationship advertised in an entity's navigation links: a server MUST NOT advertise a navigation link it does not route. ### Representation Collection responses MUST include `@iot.count` when `$count=true` is requested and a `value` array. Every entity representation MUST include `@iot.id` and `@iot.selfLink`. Related entities MUST be advertised through `@iot.navigationLink` entries unless inlined by `$expand`. ### Query options A conforming server MUST support the following system query options on every collection endpoint, with the semantics of [[OGC-STA-1]] §9.3 (aligned with [[ODATA-4]]): | Option | Requirement | | --- | --- | | `$filter` | MUST support comparison (`eq`, `ne`, `lt`, `le`, `gt`, `ge`), logical (`and`, `or`, `not`), and the string functions `startswith`, `endswith`, `contains`. | | `$orderby` | MUST support one or more properties with `asc`/`desc`. | | `$select` | MUST project the response to the named properties. | | `$expand` | MUST inline related entities; nested `$expand` MUST be supported. | | `$top` | MUST limit the result set. | | `$skip` | MUST offset the result set. | | `$count` | MUST, when `true`, include `@iot.count`. | The server MUST apply a default page size and a maximum page size to `$top`. The defaults are configurable; the default page size is `100` and the maximum is `10000`. A request for `$top` greater than the configured maximum MUST be clamped to the maximum rather than rejected. The server MUST enforce a maximum `$expand` nesting depth (default `2`); a request exceeding it MUST be rejected with HTTP `400` and the OGC error envelope (). ### Observations set Because the Observations set is backed by a time-partitioned hypertable, a server MUST keep the default ordering of an Observations collection deterministic. When no `$orderby` is supplied the server SHOULD order by `phenomenonTime` descending. Servers MAY auto-resolve a `FeatureOfInterest` for an Observation from the associated Thing's Location when none is supplied, per [[OGC-STA-1]]; a server that does so MUST expose the resolved feature through the `Observations({id})/FeatureOfInterest` singleton. ### Deviations from the base standard A server in this profile MAY omit the following optional parts of [[OGC-STA-1]] without losing conformance to this profile (but it then does not conform to the full OGC standard): the `$expand` of more than the configured depth, server-driven paging via `@iot.nextLink` beyond the configured maximum, and the `MultiDatastream` entity. A server MUST document any such omission in its capabilities or accompanying documentation. ## Authentication & authorization {#auth} ### Model The server defines a pluggable authentication boundary. The [=host application=] MUST contribute an authentication middleware (default `auth:sanctum`) and MUST register two authorization gate abilities: `sta.read` and `sta.write`. A third ability, `secrets.write`, MUST additionally gate mutations of [=device secret=]s (). A conforming server MUST enforce, for every request other than the explicitly unauthenticated routes below: - a valid bearer credential, else respond `401`; and - the gate ability appropriate to the route — `sta.read` for safe methods (`GET`), `sta.write` for unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`) — else respond `403` with the OGC error envelope. ### Tokens The server issues bearer tokens through Laravel Sanctum personal-access-tokens. The following authentication endpoints MUST be provided: | Method & path | Auth | Description | | --- | --- | --- | | `POST /v1.1/auth/login` | none | Exchange `{ email, password }` for `{ token }`. | | `POST /v1.1/auth/logout` | bearer | Revoke the current token. | | `GET /v1.1/auth/me` | bearer | Return the authenticated user. | | `GET /v1.1/auth/tokens` | bearer | List the caller's tokens. | | `POST /v1.1/auth/tokens` | bearer | Create a named token, returning `{ token }`. | | `DELETE /v1.1/auth/tokens/{id}` | bearer | Revoke a token by numeric id. | `POST /v1.1/auth/login` MUST be reachable without an authentication context (it is the bootstrap endpoint) and MUST NOT be subject to the gate. All other `auth/*` routes MUST require a valid bearer token. ### Cookie bearer fallback A server MAY accept the bearer token from a cookie when no `Authorization` header is present, to support browser clients. When enabled, the cookie name is configurable (default `access_token`). Servers SHOULD disable the cookie fallback in server-to-server deployments. ## Device-Management Extension {#device-management} This section is normative for the **Device-Management Extension** conformance class. It realises the [[OPENCITYSENSE]] device-registration model. Naming follows OpenCitySense one-to-one. ### Thing extensions {#thing-extensions} A conforming server MUST extend the [=Thing=] entity with the following properties: | Property | Type | Requirement | | --- | --- | --- | | `type` | string | MUST be one of `device`, `connector`, `other`. Defaults to `device`. The server MUST validate the value at the application layer. | | `controlling_connector_id` | integer, nullable | MUST reference a [=Thing=] whose `type` is `connector`. MUST be settable only on a Thing whose `type` is `device`. On deletion of the referenced connector the server MUST restrict (reject) the delete while references exist. | The Thing entity MUST additionally expose, as navigation paths: - `Things({id})/Configurations` — collection of [=configuration=]s for the Thing. - `Things({id})/DeviceSecrets` — collection of [=device secret=]s for the Thing. - `Things({id})/TaskingCapabilities` — collection; valid only when `type=connector`. - `Things({id})/ControlledDevices` — collection of devices a connector controls; valid only when `type=connector`. - `Things({id})/ControllingConnector` — singleton; the connector managing a device; valid only when `type=device`. `type` MUST be exposed as an OData-filterable property and MUST be expandable in the five relationships above. ### DeviceModels {#device-models} A [=device model=] is a template for a class of devices. The server MUST expose a `DeviceModels` entity set with full CRUD and the following representation: | Property | Type | Requirement | | --- | --- | --- | | `name` | string | MUST be present. | | `description` | string, nullable | MAY be present. | | `config_schema` | JSON object | JSON Schema for the `payload` of a [=configuration=] of this model. Defaults to `{}`. | | `decoder_ref` | string, nullable | Opaque external reference (URI or name). The decoder entity itself is out of scope. | | `properties` | JSON object | Free map (manufacturer, model number, firmware…). Defaults to `{}`. | A DeviceModel MUST expose: - `DeviceModels({id})/Sensors` — the sensor/observed-property templates bound to the model through the `device_model_sensors` relationship. Each binding MUST carry a `unit_of_measurement` (UCUM block) and MAY carry a `name_template` used to materialise Datastream names during onboarding. - `DeviceModels({id})/Configurations` — configuration templates for the model (configurations with `status=template`). A DeviceModel MAY declare compatibility with connector device models through a self-referential `CompatibleConnectorModels` relationship; when present, the server MUST expose it via `$expand`. ### Configurations {#configurations} A [=configuration=] records the operating parameters applied to a device. The server MUST expose a `Configurations` entity set with full CRUD and the following representation: | Property | Type | Requirement | | --- | --- | --- | | `thing_id` | integer, nullable | The device. MUST be null when `status=template`; MUST be present otherwise. | | `device_model_id` | integer | MUST reference a [=device model=]. | | `status` | string | MUST be one of `created`, `active`, `inactive`, `removed`, `template`. | | `valid_from` | timestamp | When the configuration took effect. | | `payload` | JSON object | MUST validate against the referenced device model's `config_schema`. | | `schema_snapshot` | JSON object | A copy of `config_schema` at creation time, for audit. The server MUST populate it on create and MUST NOT mutate it thereafter. | The server MUST enforce the application rule that **at most one** Configuration per `thing_id` has `status=active`. A configuration with `status=template` MUST have a null `thing_id`. The server MUST expose `Things({id})/Configurations` as the device-scoped collection. ### DeviceSecrets {#device-secrets} A [=device secret=] is a credential the server stores **only as ciphertext**. The server MUST expose a `DeviceSecrets` entity set with the following representation: | Property | Type | Requirement | | --- | --- | --- | | `thing_id` | integer | MUST reference the owning device. | | `name` | string | E.g. `appkey`, `nwkkey`, `api_token`. Unique per `(thing_id, name)`. | | `type` | string | E.g. `appkey`, `oauth_token`, `password`. | | `value_encrypted` | string | Base64 of a libsodium `crypto_box_seal` ciphertext, sealed to the connector's public key ([[NACL]]). | | `pubkey_fingerprint` | string | SHA-256 hex of the connector public key used to seal the value, for audit. | The server MUST NOT be able to decrypt `value_encrypted`; it MUST NOT store, log or return the plaintext. The following confidentiality rules are normative: - A **collection** response (`GET /v1.1/DeviceSecrets`, `Things({id})/DeviceSecrets`) MUST omit `value_encrypted`. - A **single** entity response MUST omit `value_encrypted` unless the request carries the explicit `include=ciphertext` query parameter, in which case the server MUST return the ciphertext (never plaintext). - Reads MUST require `sta.read`. Every mutation (`POST`, `PUT`, `PATCH`, `DELETE`) MUST require both `sta.write` and `secrets.write`. ### Storage requirements {#dm-storage} The extension entities MUST be persisted on the `pgsql_sta` connection. The server MUST enforce the uniqueness constraints stated above (`(device_model_id, sensor_id, observed_property_id)` for sensor bindings, `(thing_id, name)` for secrets) at the database layer. Referential actions MUST follow: cascade on `thing_id`, restrict on `device_model_id` and on `controlling_connector_id`. ## Tasking Subset {#tasking} This section is normative for the **Tasking Subset** conformance class. It implements a minimal subset of [[OGC-STA-2]]: the `TaskingCapability` and `Task` entities only. The `Actuator` entity is out of scope; the action to perform is carried in `taskingParameters.action`. ### TaskingCapabilities {#tasking-capabilities} A [=tasking capability=] declares the task actions a connector accepts. The server MUST expose a `TaskingCapabilities` entity set with full CRUD and: | Property | Type | Requirement | | --- | --- | --- | | `name` | string | E.g. `connector-ttn-rotterdam.onboard`. | | `description` | string, nullable | MAY be present. | | `thing_id` | integer | MUST reference a [=Thing=] whose `type` is `connector`. | | `tasking_parameters` | JSON object | A schema (SWE-DataRecord-like, free object) describing the `taskingParameters` a Task may carry. | | `properties` | JSON object | Free map. | The server MUST expose `TaskingCapabilities({id})/Tasks` and `Things({id})/TaskingCapabilities`. A connector Thing typically declares one capability supporting the actions `OnboardDevice`, `OffboardDevice`, `UpdateConfiguration` and `RotateSecret`. ### Tasks {#tasks} A [=task=] is a unit of work for a connector. The server MUST expose a `Tasks` entity set with full CRUD and: | Property | Type | Requirement | | --- | --- | --- | | `tasking_capability_id` | integer | MUST reference a [=tasking capability=]. | | `creation_time` | timestamp | OGC `creationTime`; defaults to now. | | `tasking_parameters` | JSON object | MUST contain `action` and `thingId`; MAY contain action-specific parameters. | | `status` | string | MUST be one of `created`, `pending`, `running`, `completed`, `failed`. Defaults to `created`. | | `result` | JSON object, nullable | On `completed`/`failed`, MUST carry a message or details. | | `started_at` | timestamp, nullable | Set when a connector picks the task up. | | `completed_at` | timestamp, nullable | Set on terminal status. | The server itself MUST NOT execute tasks; execution is the connector's responsibility. The server's role is to persist the task, emit the notification () and expose the status for polling. ### Task notification over MQTT {#mqtt} On `POST /v1.1/Tasks`, or on a `PATCH /v1.1/Tasks({id})` that transitions `status` to `pending`, a conforming server MUST publish a notification to the MQTT topic: ``` v1.1/Tasks/{taskingCapabilityId} ``` The message MUST be JSON, with QoS 1 and the retain flag clear, and MUST carry at least the task id, creation time, status and tasking parameters: ```json { "@iot.id": 123, "creationTime": "2026-05-22T10:11:12Z", "status": "pending", "taskingParameters": { "action": "OnboardDevice", "thingId": 456 }, "taskingCapability@iot.navigationLink": "/v1.1/TaskingCapabilities(7)" } ``` The topic prefix (`v1.1`) MUST be configurable. The broker connection (host, port, TLS, credentials) MUST be configurable through the environment. If no broker is configured or the broker is unreachable, the server MUST NOT fail the write: it MUST complete the `POST`/`PATCH` and SHOULD log a warning. In that case a connector falls back to polling `GET /v1.1/Tasks?$filter=status eq 'pending'`. A server MUST therefore keep the queryable `status` field authoritative independently of whether the notification was delivered. ## Management Interface {#management} This section is normative for the **Management Interface** conformance class. These endpoints are not part of SensorThings; they support operation and the portal. ### Health {#health} `GET /v1.1/health` MUST return `200` with at least `{ "status", "db", "version" }` when the server and its database are reachable. It MUST require the host authentication context but MUST NOT require the `sta.read` gate, so a portal can probe liveness without data access. It MUST verify database reachability before reporting `db: "ok"`. ### Statistics {#stats} `GET /v1.1/stats` MUST return per-entity-set row counts for dashboards and MUST require `sta.read`. ### Conformance runner {#conformance-runner} The server MUST expose an HTTP wrapper around its OGC conformance test runner: | Method & path | Gate | Description | | --- | --- | --- | | `POST /v1.1/conformance/runs` | `sta.write` | Start a run asynchronously; returns a run id (UUID). | | `GET /v1.1/conformance/runs` | `sta.read` | List runs. | | `GET /v1.1/conformance/runs/{id}` | `sta.read` | Run metadata by id. | | `GET /v1.1/conformance/runs/{id}/report.html` | `sta.read` | HTML report. | Runs are asynchronous; a host MUST run a queue worker for runs to complete. ### Server configuration {#server-config} `GET /v1.1/config` (`sta.read`) MUST return the effective server configuration (defaults plus runtime overrides). `POST /v1.1/config` (`sta.write`) MUST apply a runtime override without requiring a redeploy. The server MUST persist overrides and MUST apply them ahead of the static defaults. ### Connector proxy {#connector-proxy} The server MUST provide a read-mostly proxy to an on-premises connector's `/admin/*` management API so that the connector's admin token never reaches the browser. Routes are grouped under `GET|POST|PUT|PATCH|DELETE /v1.1/connectors/{connectorId}/…`, where `{connectorId}` is the connector [=Thing=]'s `name`. The server MUST hold the connector's admin token server-side (in configuration) and MUST inject it when forwarding — except for `/connectors/{connectorId}/info`, which the connector serves ungated and which the server MUST forward without a bearer. Read forwards MUST require `sta.read`; provisioning forwards that use unsafe verbs (source create/replace/patch/delete and the lifecycle actions) MUST require `sta.write`. The proxied surface is specified normatively by the *STA Connector — Technical Specification*; this server MUST treat it as an opaque forward target and MUST NOT reinterpret connector responses. ## Error handling {#errors} All error responses MUST use the OGC error envelope: ```json { "code": 404, "description": "Thing(99) not found" } ``` A conforming server MUST use the following HTTP status codes with their stated meaning: `200` (ok), `201` (created), `204` (deleted / preflight), `400` (malformed request or query option), `401` (missing or invalid credential), `403` (insufficient ability), `404` (unknown entity), `422` (validation failure), `500` (server error). The `code` field in the envelope MUST equal the HTTP status code. ## CORS {#cors} A conforming server MUST default to **no** CORS headers (server-to-server mode). When the allowed-origins configuration is non-empty, the server MUST emit `Access-Control-Allow-Origin` for matching origins and MUST answer `OPTIONS` preflight requests with `204` and the appropriate CORS headers. The wildcard `*` MUST be permitted only when credentialed requests are disabled. ## Security & privacy considerations {#security} - **Secret confidentiality.** [=device secret=]s MUST be sealed to a connector public key with `crypto_box_seal` ([[NACL]]) before they reach the server. The server MUST hold only ciphertext and MUST follow the disclosure rules of . A compromise of the server database MUST NOT reveal secret plaintext. - **Token handling.** Bearer tokens MUST be transmitted only over TLS in production. The cookie-bearer fallback () widens the browser attack surface and SHOULD be disabled where not required. - **Authorization granularity.** The split between `sta.read`, `sta.write` and the additional `secrets.write` ability means a read-only or non-secret-bearing client can be issued a token that cannot mutate secrets. Hosts SHOULD issue least-privilege tokens. - **Connector token isolation.** The connector proxy () exists so that a connector's admin token is never exposed to the browser; implementations MUST keep that token server-side. - **MQTT exposure.** Task notifications are published to a broker; the payload carries task metadata but MUST NOT carry secret plaintext. Brokers SHOULD require TLS and authentication.
## Conformance testing The implementation is validated by an automated test suite (Pest) executed against a real PostgreSQL + TimescaleDB + PostGIS instance, with a line-coverage gate of 85% on `src/`. An OGC conformance test runner () exercises the server against the published OGC test scenarios and produces an HTML report; the reference [[FROST|FROST-Server]] is used as a behavioural oracle for ambiguous cases. An OpenAPI document is generated from the routes and checked for drift in CI. A claim of conformance to any class in SHOULD be accompanied by a passing conformance report for that class.
## Examples ### RQ3 device onboarding (end-to-end) The onboarding flow exercises the Device-Management and Tasking classes together. The portal performs the writes; the connector reacts to the MQTT notification. ``` Portal Server Connector │ POST Thing (type=device) │ │ ├──────────────────────────► │ │ │ POST Configuration │ │ ├──────────────────────────► │ │ │ POST DeviceSecret[] │ │ │ (client-side encrypted) │ │ ├──────────────────────────► │ │ │ POST Task │ MQTT publish │ │ (action=OnboardDevice) │ v1.1/Tasks/{capabilityId} │ ├──────────────────────────► ├─────────────────────────────►│ │ │ PATCH Task running │ │ │ GET Thing $expand=… │ │ │ PATCH Thing controlling… │ │ │ POST Datastreams[] │ │ │ PATCH Config active │ │ GET Task({id}) (poll) │ PATCH Task completed │ │ ◄──────────────────────────┤ │ ``` ### A filtered, expanded query ``` GET /v1.1/Datastreams?$filter=Thing/id eq 5 &$expand=Observations($top=10;$orderby=phenomenonTime desc) &$select=id,name&$count=true ```
## References Normative and informative references are listed below; ReSpec resolves RFC entries automatically and the OGC, Geonovum and libsodium entries from this document's local bibliography. Key normative references: [[OGC-STA-1]], [[OGC-STA-2]], [[RFC2119]], [[RFC8174]], [[RFC3339]], [[NACL]]. Key informative references: [[GEONOVUM-TESTBED]], [[OPENCITYSENSE]], [[FROST]] (the reference implementation), [[OM]], [[ODATA-4]].
## Implementation notes & rationale The design decisions behind this implementation are recorded as Architecture Decision Records in `docs/adr/` of the repository (license, package-vs-app, the TimescaleDB hypertable, the OData parser architecture, the historical-location trigger, the FeatureOfInterest auto-resolution, the conformance runner, and the management endpoints). Those ADRs are informative background and are not part of the normative contract; where an ADR and this specification disagree, this specification governs.