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
MessageTypeIddecides which handler executes. -
Strict contracts: Every
MessageTypeIdhas 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.
- Authorization runs before validation and uses only header metadata (
-
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
Payloadobject 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 optionaldetails(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
successappears first inside everyPayloadfor quick outcome checks.
- JSON objects are unordered, but
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
nullwhen unused (never omitted).
- Responses: All defined fields are present; unknown/unset values are
-
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
Payloadtop 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).
- Single embedded objects use singular (
4) Filters, Time, Geo & Pagination
-
Standardized filter names (requests):
- Time range:
from,to(ISO 8601). - Geo:
lat,lng,radius. - Pagination:
limit,offset.
- Time range:
-
Units (global conventions):
- Distances: meters
- Angles: degrees
- Coordinates: degrees (
lat,lng) - Durations: seconds
(If any
MessageTypeIdneeds different units later, it must use an explicitly named field likeradiusKm.)
-
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
limitandoffset. -
Responses always include:
{ "limit": <int>, "offset": <int>, "totalCount": <int>, "nextCursor": null }(
nextCursoris reserved for future cursor pagination; set tonullin 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).
- Fixed per
-
Streaming/chunked responses: Not supported in v1 (pagination only).
5) Request Processing Pipeline (Server Behavior)
-
Receive request with envelope.
-
Extract header fields:
MessageTypeId,DeviceId,Timestamp,Version, optionalrequestId. -
Authorization (Rego/OPA):
- Uses only header metadata (
MessageTypeId+ authenticated identity). - If not allowed → respond with FORBIDDEN (no hints).
- Uses only header metadata (
-
Validation (if authorized):
- Validate
DeviceIdequals token’s Zitadel ID. - Validate
Versionequals"v1". - Validate
Payloadagainst the request schema for thatMessageTypeId.
- Validate
-
Execution:
- Dispatch to the mapped handler (SQL or custom).
- For SQL: bind parameters, execute, map columns, apply transforms, add virtual fields as configured.
-
Response validation:
- Validate the outgoing
Payloadagainst the response schema (enforced in production).
- Validate the outgoing
-
Logging:
- Log structured entry (with censoring rules applied).
-
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- Request JSON Schema
- Response JSON Schema
- 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: truein 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: trueis 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 thePayload(with censoring applied). -
Censoring controls:
-
Per-
MessageTypeIdcensoredlist 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-configloads 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 aMessageTypeId.
11) Miscellaneous
-
Errors: No global catalog; each
MessageTypeIddefines its ownerrorCodeoptions (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.sqlConfig (/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
}
}