IQ FireWatch LogoIQ FireWatch Docs

REST

Internal development documentation for the IQ FireWatch platform.

Welcome to the internal documentation site for IQ FireWatch.

This site contains technical and operational information for developers, engineers, and deployment teams working on the IQ FireWatch platform.

API Specification (v1)

0) Overview & Operating Model

  • Style: RPC over HTTP with a single POST-style endpoint (the exact path is implementation-specific).

  • Dispatch: The request’s MessageTypeId decides which handler executes.

  • Strict contracts: Every MessageTypeId has two JSON Schemas (request + response) and a human-readable example.

  • Security-first:

    • Authorization runs before validation and uses only header metadata (MessageTypeId + authenticated identity).
    • If not authorized → return FORBIDDEN with no hints.
    • If authorized → perform strict schema validation and then execute the mapped handler.
  • Schema-driven + Config-driven: Adding features is primarily adding schemas + config + SQL (no code changes for pure SQL handlers).


1) Message Envelope

Every HTTP request and response uses the same top-level JSON wrapper:

{
  "MessageTypeId": "<string, required>",
  "DeviceId": "<zitadelId, string, required>",
  "Timestamp": "<ISO 8601, required>",
  "Version": "v1",
  "requestId": "<UUID, optional in request, always present in response>",
  "Payload": { ... }
}

1.1 Field Semantics

  • MessageTypeId

    • Identifies the RPC action.
    • Maps to schemas, handler type, SQL statement (if any), mappings, and behavior in config.
    • Error responses echo the same MessageTypeId.
  • DeviceId

    • Client must include their Zitadel ID here.
    • Server cross-checks it against the authenticated token subject; mismatches are rejected.
    • Purpose: symmetry with the WebSocket format and an extra integrity check.
  • Timestamp

    • Client-provided ISO 8601 timestamp (UTC or any valid offset).
    • Server logs this alongside server reception time.
    • Clock skew does not cause rejection in v1; anomalies are logged for investigation.
  • Version

    • Global API version string.
    • v1 requires an exact "v1" match; otherwise requests are rejected.
    • No per-message schema versioning in v1.
  • requestId

    • Optional in request. If provided, must be a UUID and is echoed back.
    • If omitted, the server generates one and includes it in the response.

2) Payload Conventions

  • The Payload object always begins with:

    { "success": true|false, ... }

    (This is present on both requests and responses; on the request side it is validated according to the per-message request schema.)

  • On success responses:

    • success: true
    • Data shape defined by the response schema for that MessageTypeId.
  • On error responses:

    • success: false
    • Include errorCode, errorMessage, and optional details (values enumerated per message type’s response schema).
  • Authorization failures:

    • Evaluated before validation.

    • Return a minimal, uniform payload:

      { "success": false, "errorCode": "FORBIDDEN" }
    • No schema/format hints beyond the common envelope.

  • Validation failures:

    • Evaluated after authorization.
    • Provide explicit detail; verbosity is configurable per MessageTypeId.
  • Field ordering:

    • JSON objects are unordered, but success appears first inside every Payload for quick outcome checks.

3) Data Modeling Rules (API-facing)

  • Naming: All API fields use camelCase (DB naming is internal).

  • IDs: Always strings, even if numeric in DB (future-proof & safe across languages).

  • Booleans: Native JSON booleans (true/false).

  • Enums: Strings with allowed values enumerated in the JSON Schemas.

  • Nullability:

    • Responses: All defined fields are present; unknown/unset values are null (never omitted).
    • Requests: Optional filters must be present and set to null when unused (never omitted).
  • Numbers with units (responses): Where a value has a physical unit, return an object:

    { "value": 120, "unit": "meters" }

    (Unset unit-bearing values are null. Plain numeric fields without units remain numbers.)

  • Lists (responses): Always under a named array wrapper (e.g., sensors, alarms, images) and include pagination metadata (see §4).

  • Detail responses: May place fields directly at Payload top level (no extra wrapper key).

  • Cross-resource references: Include both the reference ID (e.g., sensorId) and the expanded object (e.g., sensor).

  • Singular vs. plural:

    • Single embedded objects use singular (sensor, fireStation, user).
    • Collections use plural (images, linkedSmokes, roles).

4) Filters, Time, Geo & Pagination

  • Standardized filter names (requests):

    • Time range: from, to (ISO 8601).
    • Geo: lat, lng, radius.
    • Pagination: limit, offset.
  • Units (global conventions):

    • Distances: meters
    • Angles: degrees
    • Coordinates: degrees (lat, lng)
    • Durations: seconds (If any MessageTypeId needs different units later, it must use an explicitly named field like radiusKm.)
  • Time handling:

    • Requests may include UTC or offset-based ISO 8601 timestamps.
    • Server normalizes times internally (e.g., to UTC), while logging the original client time.
  • Pagination (v1 standard):

    • Requests can include limit and offset.

    • Responses always include:

      { "limit": <int>, "offset": <int>, "totalCount": <int>, "nextCursor": null }

      (nextCursor is reserved for future cursor pagination; set to null in v1.)

    • Invalid numeric values are rejected with validation errors (no auto-correction).

  • Ordering:

    • Fixed per MessageTypeId, not set by the client.
    • Defined centrally via config (see §6).
  • Streaming/chunked responses: Not supported in v1 (pagination only).


5) Request Processing Pipeline (Server Behavior)

  1. Receive request with envelope.

  2. Extract header fields: MessageTypeId, DeviceId, Timestamp, Version, optional requestId.

  3. Authorization (Rego/OPA):

    • Uses only header metadata (MessageTypeId + authenticated identity).
    • If not allowed → respond with FORBIDDEN (no hints).
  4. Validation (if authorized):

    • Validate DeviceId equals token’s Zitadel ID.
    • Validate Version equals "v1".
    • Validate Payload against the request schema for that MessageTypeId.
  5. Execution:

    • Dispatch to the mapped handler (SQL or custom).
    • For SQL: bind parameters, execute, map columns, apply transforms, add virtual fields as configured.
  6. Response validation:

    • Validate the outgoing Payload against the response schema (enforced in production).
  7. Logging:

    • Log structured entry (with censoring rules applied).
  8. Respond with the common envelope and validated Payload.


6) Configuration (Declarative)

  • A single YAML config maps MessageTypeId → handler & behavior.

  • Handlers:

    • sql → execute a named SQL file.
    • custom → run a named Go function.
  • Explicit column→field mapping (DB → API) — no automatic mapping:

    • Direct field mapping: db_column: apiFieldName

    • With transform:

      db_column:
        field: apiFieldName
        transform: toIso8601
    • Virtual fields (not from SQL):

      _virtual:
        fieldName: ComputeFunctionName
  • Other config options:

    • orderBy: fixed ordering (e.g., "last_ping DESC").
    • write: true: mark the operation as write-capable (default read-only).
    • censored: list of API field names to mask in logs.
    • allowIncomplete: true: in dev, allow startup with missing/invalid artifacts (endpoint disabled with warnings).

Example:

LIST_MY_SENSORS:
  handler: sql
  statement: "list-my-sensors"
  mapping:
    sid: sensorId
    type_name: typeName
    last_ping:
      field: lastPing
      transform: "toIso8601"
    _virtual:
      sensorHealth: "ComputeSensorHealth"
  orderBy: "last_ping DESC"
  write: false
  censored:
    - "sensorId"

7) Schemas (Directory, Contents & Validation)

  • Per MessageTypeId: three artifacts

    1. Request JSON Schema
    2. Response JSON Schema
    3. Human-readable example
  • Location: local filesystem, domain-based but flat structure:

    /schemas/<domain>/<message-type>.request.json
    /schemas/<domain>/<message-type>.response.json
  • Validation:

    • Requests and responses must conform exactly (validated in production).
    • No preprocessing before validation — raw input is authoritative.
    • Error detail verbosity is configurable per MessageTypeId.

8) SQL (Files, Safety & Startup Checks)

  • One statement per file, kebab-case filenames, grouped by domain:

    /sql/<domain>/<message-type>.sql
  • Parameterization only ($1, $2, …). No string interpolation.

  • Read vs. write:

    • Default: read-only.
    • Must explicitly set write: true in config to allow data modification.
  • Transactions: Orchestrated in server logic (can combine multiple statements); no transaction blocks inside SQL files.

  • Startup verification:

    • SQL files are validated at startup (syntax, table/column presence, placeholders) against the current DB schema.
    • Missing/invalid artifacts → startup fails by default.
    • If allowIncomplete: true is set for a message type → server warns and disables that message type instead of failing.

9) Logging (Structured + Censoring)

  • Structured JSON logs for all requests/responses.

  • Include: requestId, MessageTypeId, authenticated IDs (e.g., uid), deviceId, timestamps, and the Payload (with censoring applied).

  • Censoring controls:

    • Per-MessageTypeId censored list in config masks those API fields in logs with "[CENSORED]".

    • Startup flag controls behavior:

      • respect-censored (default): apply per-message censor rules.
      • force-full: log full payloads (including fields otherwise censored).

10) Deployment & Lifecycle

  • Artifacts bundled in the server image: config, schemas, SQL.
  • No hot reload: artifacts load only at startup.
  • Startup validation: full consistency checks across config ↔ schemas ↔ SQL.
  • Dry-run mode: --validate-config loads and validates everything, then exits (useful for pre-deployment checks).
  • DB migrations: handled separately (outside the API server).
  • Health: infrastructure-level endpoint (e.g., /healthz) — not a MessageTypeId.

11) Miscellaneous

  • Errors: No global catalog; each MessageTypeId defines its own errorCode options (enumerated in its response schema).

  • Deprecations: Responses may include:

    "deprecated": true,
    "deprecationMessage": "Field 'foo' will be removed in v2"
  • Batching: Not supported in v1 (strict one request → one response).

  • Compression: rely on standard HTTP transport compression only.

  • Binary data: Return URLs (e.g., S3-compatible) — never inline base64 content.

  • Hypermedia links: Not included; responses remain lean (IDs + expanded objects as needed).


Dummy Example: LIST_MY_SENSORS

Directory Layout

/config/
  message-types.yaml
/schemas/
  /sensors/
    list-my-sensors.request.json
    list-my-sensors.response.json
/sql/
  /sensors/
    list-my-sensors.sql

Config (/config/message-types.yaml)

LIST_MY_SENSORS:
  handler: sql
  statement: "list-my-sensors"
  mapping:
    sid: sensorId
    type_name: typeName
    last_ping:
      field: lastPing
      transform: "toIso8601"
  orderBy: "last_ping DESC"
  write: false
  censored:
    - "sensorId"

Request Schema (/schemas/sensors/list-my-sensors.request.json)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ListMySensorsRequest",
  "type": "object",
  "properties": {
    "success": { "const": true },
    "from": { "type": ["string", "null"], "format": "date-time" },
    "to": { "type": ["string", "null"], "format": "date-time" },
    "limit": { "type": "integer", "minimum": 1, "maximum": 100 },
    "offset": { "type": "integer", "minimum": 0 }
  },
  "required": ["success", "from", "to", "limit", "offset"]
}

Response Schema (/schemas/sensors/list-my-sensors.response.json)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ListMySensorsResponse",
  "type": "object",
  "properties": {
    "success": { "type": "boolean" },
    "sensors": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "sensorId": { "type": "string" },
          "typeName": { "type": "string" },
          "lastPing": { "type": ["string", "null"], "format": "date-time" }
        },
        "required": ["sensorId", "typeName", "lastPing"]
      }
    },
    "limit": { "type": "integer" },
    "offset": { "type": "integer" },
    "totalCount": { "type": "integer" },
    "nextCursor": { "type": ["string", "null"] }
  },
  "required": ["success", "sensors", "limit", "offset", "totalCount", "nextCursor"]
}

SQL (/sql/sensors/list-my-sensors.sql)

SELECT s.sid, st.type_name, s.last_ping
FROM sensor s
JOIN user_sensor us ON s.sid = us.sid
JOIN sensor_type st ON s.stid = st.stid
WHERE us.uid = $1
ORDER BY s.last_ping DESC
LIMIT $2 OFFSET $3;

Example Request

{
  "MessageTypeId": "LIST_MY_SENSORS",
  "DeviceId": "423095324234235",
  "Timestamp": "2025-08-04T14:00:00Z",
  "Version": "v1",
  "Payload": {
    "success": true,
    "from": null,
    "to": null,
    "limit": 20,
    "offset": 0
  }
}

Example Response

{
  "MessageTypeId": "LIST_MY_SENSORS",
  "DeviceId": "423095324234235",
  "Timestamp": "2025-08-04T14:00:01Z",
  "Version": "v1",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "Payload": {
    "success": true,
    "sensors": [
      { "sensorId": "S1", "typeName": "Camera", "lastPing": "2025-08-01T13:45:00Z" },
      { "sensorId": "S2", "typeName": "Radar", "lastPing": null }
    ],
    "limit": 20,
    "offset": 0,
    "totalCount": 523,
    "nextCursor": null
  }
}