# Event Wire Contract This document is the canonical JSON contract for events emitted by `weatherfeeder` JSON sinks, including stdout and NATS. Postgres stores the same event envelope fields in parent table columns; see [Postgres integration](postgres.md). Downstream consumers should read the envelope, switch on `schema`, and decode `payload` according to that schema. ## Envelope Every emitted event is a JSON object with these fields: | Field | Type | Required | Notes | |---|---|:---:|---| | `id` | string | yes | Stable event identifier. Treat as opaque. | | `kind` | string | yes | Routing kind, such as `observation` or `alert`. | | `source` | string | yes | Configured source name. | | `emitted_at` | timestamp | yes | When the daemon emitted the event. | | `effective_at` | timestamp | no | Timestamp the payload is about, when known. | | `schema` | string | no | Schema identifier. Weatherfeeder sources and normalizers set this. | | `payload` | object, array, string, or scalar | yes | Schema-specific payload. | Timestamps are JSON strings using Go `time.Time` JSON encoding, which is RFC3339Nano-compatible. Weatherfeeder normalizers use UTC timestamps for canonical payloads. ## Kinds And Schemas Canonical schemas emitted after normalization: | Kind | Schema | Payload | |---|---|---| | `observation` | `weather.observation.v1` | `WeatherObservation` | | `forecast` | `weather.forecast.v1` | `WeatherForecastRun` | | `forecast_discussion` | `weather.forecast_discussion.v1` | `WeatherForecastDiscussion` | | `weather_story` | `weather.weather_story.v1` | `WeatherStoryRun` | | `alert` | `weather.alert.v1` | `WeatherAlertRun` | Raw upstream schemas emitted by current sources: | Kind | Schema | Payload | |---|---|---| | `observation` | `raw.nws.observation.v1` | NWS observation JSON | | `observation` | `raw.openmeteo.current.v1` | Open-Meteo current JSON | | `observation` | `raw.openweather.current.v1` | OpenWeather current JSON | | `forecast` | `raw.nws.hourly.forecast.v1` | NWS hourly forecast JSON | | `forecast` | `raw.nws.narrative.forecast.v1` | NWS narrative forecast JSON | | `forecast_discussion` | `raw.nws.forecast_discussion.v1` | NWS forecast discussion HTML string | | `weather_story` | `raw.nws.weatherstories.v1` | NWS weather stories JSON | | `forecast` | `raw.openmeteo.hourly.forecast.v1` | Open-Meteo hourly forecast JSON | | `alert` | `raw.nws.alerts.v1` | NWS alerts JSON | `standards.SchemaRawOpenWeatherHourlyForecastV1` exists in code, but no current registered source emits it. ## Shared Conventions - Canonical numeric measurements use metric units. - Floating-point values in canonical payloads are rounded to 4 digits after the decimal point during normalization. - Optional fields use JSON `omitempty`; absent fields should be treated as unknown. - `conditionCode` is a WMO weather interpretation code. Unknown observation conditions use `-1`. Forecast period `conditionCode` is optional. - Additive fields are compatible within a schema version. Removing, renaming, or changing the meaning of a field requires a new schema identifier. ## `weather.observation.v1` Payload type: `WeatherObservation`. | Field | Type | Required | Notes | |---|---|:---:|---| | `stationId` | string | no | Provider station/location identifier. | | `stationName` | string | no | Human station name. | | `timestamp` | timestamp | yes | Observation timestamp. | | `conditionCode` | integer | yes | WMO code; `-1` means unknown. | | `isDay` | boolean | no | Day/night hint. | | `textDescription` | string | no | Short human description. | | `temperatureC` | number | no | Celsius. | | `dewpointC` | number | no | Celsius. | | `windDirectionDegrees` | number | no | Degrees. | | `windSpeedKmh` | number | no | Kilometers per hour. | | `windGustKmh` | number | no | Kilometers per hour. | | `barometricPressurePa` | number | no | Pascals. | | `visibilityMeters` | number | no | Meters. | | `relativeHumidityPercent` | number | no | Percent from 0 to 100. | | `apparentTemperatureC` | number | no | Celsius. | | `presentWeather` | array | no | Provider-specific present weather fragments. | `presentWeather[]` entries contain optional `raw` objects. ## `weather.forecast.v1` Payload type: `WeatherForecastRun`. | Field | Type | Required | Notes | |---|---|:---:|---| | `locationId` | string | no | Provider location identifier. | | `locationName` | string | no | Human location name. | | `issuedAt` | timestamp | yes | When the forecast run was generated or issued. | | `updatedAt` | timestamp | no | Later provider update time. | | `product` | string | yes | `hourly`, `narrative`, or `daily`. | | `latitude` | number | no | Degrees. | | `longitude` | number | no | Degrees. | | `elevationMeters` | number | no | Meters. | | `periods` | array | yes | Ordered forecast periods. | `periods[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `startTime` | timestamp | yes | Period start. | | `endTime` | timestamp | yes | Period end. | | `name` | string | no | Human label. | | `isDay` | boolean | no | Day/night hint. | | `conditionCode` | integer | no | WMO code when applicable. | | `textDescription` | string | no | Human summary. | | `temperatureC` | number | no | Celsius. | | `temperatureCMin` | number | no | Celsius. | | `temperatureCMax` | number | no | Celsius. | | `dewpointC` | number | no | Celsius. | | `relativeHumidityPercent` | number | no | Percent from 0 to 100. | | `windDirectionDegrees` | number | no | Degrees. | | `windSpeedKmh` | number | no | Kilometers per hour. | | `windGustKmh` | number | no | Kilometers per hour. | | `barometricPressurePa` | number | no | Pascals. | | `visibilityMeters` | number | no | Meters. | | `apparentTemperatureC` | number | no | Celsius. | | `cloudCoverPercent` | number | no | Percent from 0 to 100. | | `probabilityOfPrecipitationPercent` | number | no | Percent from 0 to 100. | | `precipitationAmountMm` | number | no | Liquid-equivalent millimeters. | | `snowfallDepthMm` | number | no | Millimeters. | | `uvIndex` | number | no | Unitless index. | ## `weather.forecast_discussion.v1` Payload type: `WeatherForecastDiscussion`. | Field | Type | Required | Notes | |---|---|:---:|---| | `officeId` | string | no | NWS office identifier. | | `officeName` | string | no | Office name. | | `product` | string | yes | Current value is `afd`. | | `issuedAt` | timestamp | yes | Bulletin issue time. | | `updatedAt` | timestamp | no | Later update time. | | `keyMessages` | array of strings | no | Extracted key messages. | | `shortTerm` | object | no | Short-term section. | | `longTerm` | object | no | Long-term section. | `shortTerm` and `longTerm` sections contain optional `qualifier`, `issuedAt`, and `text` fields. ## `weather.weather_story.v1` Payload type: `WeatherStoryRun`. | Field | Type | Required | Notes | |---|---|:---:|---| | `officeId` | string | no | NWS office identifier. | | `asOf` | timestamp | yes | Snapshot time. | | `stories` | array | yes | Ordered story cards. | `stories[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `officeId` | string | no | Office identifier. | | `startTime` | timestamp | yes | Story start. | | `endTime` | timestamp | yes | Story end. | | `updatedAt` | timestamp | yes | Story update time. | | `title` | string | no | Story title. | | `description` | string | no | Story description. | | `altText` | string | no | Image alternate text. | | `priority` | boolean | yes | Provider priority flag. | | `order` | integer | yes | Provider display order. | | `downloadUrl` | string | no | Story image URL. | ## `weather.alert.v1` Payload type: `WeatherAlertRun`. | Field | Type | Required | Notes | |---|---|:---:|---| | `locationId` | string | no | Provider location identifier. | | `locationName` | string | no | Human location name. | | `asOf` | timestamp | yes | Snapshot time. | | `latitude` | number | no | Degrees. | | `longitude` | number | no | Degrees. | | `alerts` | array | yes | Active alerts. | `alerts[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `id` | string | yes | Provider-stable alert identifier. | | `event` | string | no | Alert event label. | | `headline` | string | no | Alert headline. | | `severity` | string | no | Provider severity. | | `urgency` | string | no | Provider urgency. | | `certainty` | string | no | Provider certainty. | | `status` | string | no | Alert status. | | `messageType` | string | no | Alert message type. | | `category` | string | no | Alert category. | | `response` | string | no | Recommended response. | | `description` | string | no | Alert description. | | `instruction` | string | no | Alert instruction. | | `sent` | timestamp | no | Provider sent time. | | `effective` | timestamp | no | Effective time. | | `onset` | timestamp | no | Onset time. | | `expires` | timestamp | no | Expiration time. | | `areaDescription` | string | no | Affected area description. | | `senderName` | string | no | Provider sender name. | | `references` | array | no | Related alerts. | `references[]` entries contain optional `id`, `identifier`, `sender`, and `sent` fields. ## Compact Example ```json { "id": "NWSObservationKSTL:2026-06-10T12:00:00Z", "kind": "observation", "source": "NWSObservationKSTL", "emitted_at": "2026-06-10T12:00:05Z", "effective_at": "2026-06-10T12:00:00Z", "schema": "weather.observation.v1", "payload": { "stationId": "KSTL", "timestamp": "2026-06-10T12:00:00Z", "conditionCode": 0, "temperatureC": 22.5 } } ```