# Weatherfeeder Postgres Contract `weatherapi` reads weather data from Postgres tables owned and populated by `weatherfeeder`. This document describes only the storage contract consumed by `weatherapi`; it is not a full weatherfeeder schema reference. ## Version `go.mod` depends on: - `gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0` The repository code also depends on weatherfeeder canonical model types. Table compatibility must match the SQL in `internal/adapters/outbound/postgres`. For convective outlooks, `weatherapi` assumes weatherfeeder's `weather.outlook.v2` table reset has already been applied. ## Boundary `weatherapi` is read-only: - it does not create tables; - it does not migrate tables; - it does not ingest provider data; - it does not write weatherfeeder events. Weatherfeeder owns provider polling, normalization, table shape, and writes. Postgres owns persistence, backup, restore, and availability. ## Table Families Read | Resource | Tables read | | --- | --- | | Latest observation | `observations`, `observation_present_weather` | | Current conditions | `observations` | | Active alerts | `alert_runs`, `alerts`, `alert_references` | | Hourly forecast | `forecasts`, `forecast_periods` | | Narrative forecast | `forecasts`, `forecast_periods` | | Forecast discussion | `forecast_discussions`, `forecast_discussion_key_messages` | | Weather story run | `weather_story_runs`, `weather_stories` | | Latest weather story | `weather_stories` | | Convective outlook run | `outlook_runs`, `outlooks`, `outlook_discussions` | ## Latest Row Selection Latest parent resources use these ordering rules: - observations: `observed_at DESC, event_emitted_at DESC`; - alert runs: `as_of DESC, event_emitted_at DESC`; - hourly forecasts: `product = 'hourly'`, then `issued_at DESC, event_emitted_at DESC`; - narrative forecasts: `product = 'narrative'`, then `issued_at DESC, event_emitted_at DESC`; - forecast discussions: `issued_at DESC, event_emitted_at DESC`; - weather story runs: `as_of DESC, event_emitted_at DESC`; - latest individual weather story: `updated_at DESC, as_of DESC, story_order ASC, story_index ASC`. - convective outlook runs: `as_of DESC, event_emitted_at DESC`. Current conditions aggregate numeric values from `observations` rows where `observed_at` is inside the application-provided observation window. They also use the latest row per `event_source` in that window to select `condition_code` by source-balanced WMO family consensus. ## Child Ordering Child rows are loaded separately and attached in stored order: - observation present weather: `weather_index ASC`; - alerts: `alert_index ASC`; - alert references: `alert_index ASC, reference_index ASC`; - forecast periods: `period_index ASC`; - forecast discussion key messages: `message_index ASC`; - weather stories for a run: `story_index ASC`; - outlooks for a run: `outlook_index ASC`; - outlook discussions for a run: `discussion_index ASC`. ## Columns Read The repository reads only these columns. ### `observations` `event_id`, `event_source`, `station_id`, `station_name`, `observed_at`, `condition_code`, `is_day`, `text_description`, `temperature_c`, `dewpoint_c`, `wind_direction_degrees`, `wind_speed_kmh`, `wind_gust_kmh`, `barometric_pressure_pa`, `visibility_meters`, `relative_humidity_percent`, `apparent_temperature_c`, and `event_emitted_at`. Current conditions additionally read recent `observations` values for temperature, apparent temperature, dewpoint, humidity, wind speed, wind direction, latest `is_day`, and latest condition-code candidates per `event_source`. ### `observation_present_weather` `weather_index`, `raw_text`, and `event_id`. `raw_text` is decoded as JSON when present. Empty or null raw text maps to an empty present-weather value; invalid JSON is returned as a repository error. ### `alert_runs` `event_id`, `location_id`, `location_name`, `as_of`, `latitude`, `longitude`, and `event_emitted_at`. ### `alerts` `alert_index`, `alert_id`, `event`, `headline`, `severity`, `urgency`, `certainty`, `status`, `message_type`, `category`, `response`, `description`, `instruction`, `sent`, `effective`, `onset`, `ends`, `expires`, `area_description`, `sender_name`, and `run_event_id`. ### `alert_references` `alert_index`, `reference_index`, `id`, `identifier`, `sender`, `sent`, and `run_event_id`. ### `forecasts` `event_id`, `location_id`, `location_name`, `issued_at`, `updated_at`, `product`, `latitude`, `longitude`, `elevation_meters`, and `event_emitted_at`. Only `product = 'hourly'` and `product = 'narrative'` are read by implemented routes. ### `forecast_periods` `period_index`, `start_time`, `end_time`, `name`, `is_day`, `condition_code`, `text_description`, `temperature_c`, `temperature_c_min`, `temperature_c_max`, `dewpoint_c`, `relative_humidity_percent`, `wind_direction_degrees`, `wind_speed_kmh`, `wind_gust_kmh`, `barometric_pressure_pa`, `visibility_meters`, `apparent_temperature_c`, `cloud_cover_percent`, `probability_of_precipitation_percent`, `precipitation_amount_mm`, `snowfall_depth_mm`, `uv_index`, and `run_event_id`. ### `forecast_discussions` `event_id`, `office_id`, `office_name`, `issued_at`, `updated_at`, `product`, `short_term_qualifier`, `short_term_issued_at`, `short_term_text`, `long_term_qualifier`, `long_term_issued_at`, `long_term_text`, and `event_emitted_at`. ### `forecast_discussion_key_messages` `message_index`, `message_text`, and `run_event_id`. ### `weather_story_runs` `event_id`, `office_id`, `as_of`, and `event_emitted_at`. ### `weather_stories` `story_index`, `office_id`, `start_time`, `end_time`, `updated_at`, `title`, `description`, `alt_text`, `priority`, `story_order`, `download_url`, `run_event_id`, and `as_of`. ### `outlook_runs` `event_id`, `location_id`, `location_name`, `latitude`, `longitude`, `as_of`, `issued_at`, and `event_emitted_at`. ### `outlooks` `outlook_index`, `outlook_id`, `provider`, `product`, `day`, `outlook_type`, `label`, `label_text`, `severity_rank`, `valid_from`, `valid_to`, `issued_at`, `expires_at`, `forecaster`, `source_url`, `image_url`, `contains_location`, `geometry_json`, and `run_event_id`. `geometry_json` is copied into response GeoJSON without parsing or reserializing. It must contain valid JSON. ### `outlook_discussions` `discussion_index`, `day`, `headline`, `summary`, `discussion`, `updated_at`, and `run_event_id`. ## Nullability and Time Assumptions The repository scans nullable columns with `sql.Null*` types and maps them to nil pointers or omitted zero values depending on the canonical model field. Timestamps returned by the repository are normalized to UTC. Presentation timezone conversion happens later in HTTP presenters. Missing latest parent rows return `nil, nil` from repository methods. HTTP rendering exposes this as a successful response with `data: null`. ## Compatibility Checklist Before changing weatherfeeder storage or upgrading the weatherfeeder module: - compare table names and columns with `internal/adapters/outbound/postgres`; - preserve latest-row ordering columns used by `weatherapi`; - preserve child index columns used for ordering; - preserve nullable behavior expected by row mappers; - run `go test ./internal/adapters/outbound/postgres` and affected HTTP tests. ## Related Docs - [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md) - [`docs/operations.md`](../operations.md) - [`docs/troubleshooting.md`](../troubleshooting.md)