Apps weather-intouch 2026-06-10 · 9 min read

Doing Official Weather Alerts Right: CAP, Attribution, and Extensibility

Government alerts are free and public — but "just display them" is a trap: different sources, severity scales, mandatory attribution, expiry, local time zones.

Why "just displaying alerts" is not enough

Official weather alerts are, by definition, public and free. Government agencies publish them to protect lives and property, making them an ideal data source for any weather platform. At first glance, implementation seems trivial: just find an API, download JSON or XML, store it in a database, and render it on a map or in the forecast details for the user.

However, this naive approach is a surefire way to write messy, error-prone, and unsustainable code. As soon as you try to integrate official weather alerts into an app designed to work globally, or at least across multiple countries, you run into a series of architectural and UX hurdles.

The first issue is the inconsistency of formats and classifications. Every national weather service uses its own methodology to determine the severity of events. What one service calls "moderate danger," another might label with a yellow color or level 2 out of 5. Without a unified normalization layer, it is impossible to offer a consistent user experience.

The second pitfall is managing the lifecycle of alerts. Alerts are not static data. They are continuously updated, amended, canceled, or they naturally expire. If your application cannot reliably detect that an alert has expired, or that a new message fully replaces the previous one, you risk overwhelming users with outdated information, which undermines trust in the entire platform during critical situations.

On top of that, there are strict licensing terms requiring precise source attribution, the necessity of handling time zones correctly at the event's location, and the need to translate technical texts into the user's language. All of this shows that integrating alerts requires a robust software architecture, not just a simple script to download data.

CAP: The Common Language of Alerts

To avoid developing a completely unique parser for every country, there is an international standard called CAP (Common Alerting Protocol). It is an XML format (or its JSON variant) that defines a universal structure for disseminating emergency messages and warnings about hazardous events.

The CAP standard breaks an alert down into several key segments:

  • Event: The specific type of hazard (e.g., strong wind, flood, extreme heat).
  • Severity: The level of impact on lives and property (Extreme, Severe, Moderate, Minor, Unknown).
  • Urgency: The timeframe in which action is required (Immediate, Expected, Future, Past, Unknown).
  • Certainty: The probability of the event actually occurring (Observed, Likely, Possible, Unlikely, Unknown).
  • Description and Instruction: Textual information describing the nature of the threat and recommended behavior to minimize risk.
  • Area: The geographical specification of the affected area, most commonly defined by a polygon or a set of geocodes (e.g., FIPS codes in the US).
  • Validity: Time windows defining the start (onset), effective time (effective), and expiration (expires).

Although CAP represents a huge step forward toward unification, in practice, you still encounter minor discrepancies. Different weather services implement the standard with subtle nuances, use their own XML namespaces, or interpret combinations of the severity, urgency, and certainty fields in specific ways. Our application must therefore treat CAP as a starting point, building its own validation and normalization logic on top of it.

First Source: NWS and the Geographic Gate

The US National Weather Service (NWS) was chosen as the first production source for official weather alerts in the application. The NWS provides a robust and free API covering the United States, Alaska, Hawaii, and Puerto Rico.

However, calling an external API for every user forecast request is inefficient. If a user is searching for the weather in Prague, it is completely pointless to query NWS servers in the US. For this reason, a so-called geographic gate is placed in front of the adapter call itself.

Each adapter for a specific alert source implements a method that, based on geographic coordinates (latitude and longitude), decides whether it makes sense to query that source at all.

interface AlertSource {
  readonly id: string;
  readonly attribution: string;
  covers(latitude: number, longitude: number): boolean;
  fetchAlerts(latitude: number, longitude: number): Promise<RawAlert[]>;
}

export class NwsAlertSource implements AlertSource {
  readonly id = 'nws';
  readonly attribution = 'National Weather Service (NWS)';

  covers(latitude: number, longitude: number): boolean {
    // Geographic gate limiting queries to the US and associated territories
    const isWithinUS = latitude >= 24.396308 && latitude <= 49.384358 
                    && longitude >= -125.00165 && longitude <= -66.93457;
    const isAlaska = latitude >= 51.214183 && latitude <= 71.3895 
                  && longitude >= -179.148909 && longitude <= -129.9795;
    const isHawaii = latitude >= 18.910361 && latitude <= 28.402123 
                  && longitude >= -178.334698 && longitude <= -154.806773;
    const isPuertoRico = latitude >= 17.926822 && latitude <= 18.520556 
                      && longitude >= -67.271444 && longitude <= -65.244111;

    return isWithinUS || isAlaska || isHawaii || isPuertoRico;
  }

  async fetchAlerts(latitude: number, longitude: number): Promise<RawAlert[]> {
    // Call the NWS API and download data for the given coordinates
  }
}

Thanks to this mechanism, the application core immediately knows which adapters to activate for a given location. If a user requests a location outside NWS coverage, the adapter is instantly removed from the execution chain, saving both network bandwidth and CPU time.

Normalization: One Severity Scale for All

Once we obtain the raw data from the individual adapters, the crucial phase begins: normalization. The goal is to convert the provider-specific data structures into a unified internal model that the rest of the application and the frontend can work with.

The normalization process addresses several key tasks:

  1. Unifying the severity scale: CAP distinguishes five levels of severity, but different services interpret them differently. In our internal model, we map these states to a unified four-level scale: Informational, Moderate, Severe, and Extreme.
  2. Alert deduplication: The same alert can be published in multiple messages (e.g., the original warning, a description update, a polygon refinement). The normalization layer compares unique alert identifiers and ensures that only the latest version of a given event is ever displayed to the user.
  3. Filtering inactive alerts: Alerts whose expires timestamp is already in the past relative to the current server time are automatically discarded.
  4. Prioritization: If multiple alerts are active for a single location simultaneously (e.g., high winds and a flood watch), they are sorted in descending order of severity so that the most critical ones are presented to the user first.
export enum AlertSeverity {
  Informational = 'informational',
  Moderate = 'moderate',
  Severe = 'severe',
  Extreme = 'extreme'
}

export interface NormalizedAlert {
  id: string;
  sourceId: string;
  event: string;
  severity: AlertSeverity;
  headline: string;
  description: string;
  instruction?: string;
  startsAt: Date;
  expiresAt: Date;
  attribution: string;
}

This strict typing guarantees that the rest of the application code does not need to care where the alert came from. All alerts, regardless of their original format, satisfy the same contract.

Port-Based Architecture: Adding a Source Without Touching the Core

To ensure long-term sustainability, the platform is designed according to the principles of port-based architecture (also known as Hexagonal Architecture). The application core defines interfaces (ports) through which it communicates with the outside world. Concrete implementations of these interfaces (adapters) then live at the edge of the system.

This approach is described in more detail in the article about how the platform is put together. In the context of alerts, the port is the AlertSource interface.

┌────────────────────────────────────────────────────────┐
│                     SERVICE CORE                       │
│  (WeatherAlertService - NestJS + MongoDB + Redis)      │
│                                                        │
│             Defines port: AlertSource                  │
└──────────────────────────┬─────────────────────────────┘
                           │
         ┌─────────────────┴─────────────────┐
         ▼                                   ▼
┌─────────────────┐                 ┌─────────────────┐
│  NwsAdapter     │                 │MeteoAlarmAdapter│
│  (USA)          │                 │(Europe - planned)│
│                 │                 │                 │
│ Implements      │                 │ Implements      │
│ AlertSource     │                 │ AlertSource     │
└─────────────────┘                 └─────────────────┘

If we decide to add support for the European MeteoAlarm system or the Czech CHMI in the future, we won't have to modify a single line in the core WeatherAlertService. We will simply create a new adapter (e.g., MeteoAlarmAdapter) that implements the AlertSource interface, register it in the DI container, and the system will automatically start fetching, normalizing, and displaying alerts for European locations.

This loose coupling makes testing significantly easier. When writing unit and integration tests for the service core, we do not need to make actual network calls to NWS servers. We can simply swap in a mock adapter that returns predefined test data.

Mandatory Attribution and Local Time Zones

When displaying official alerts, legal and UX requirements cannot be ignored. Most data providers (including public ones) require clear attribution of the information source under their licenses (e.g., CC BY).

Therefore, in the platform's user interface, attribution is strictly required for every displayed alert. The user must see at a glance which authority issued the alert. Consequently, the attribution field is a mandatory part of every normalized record in both the database and API responses.

The second, technically much more challenging hurdle is the correct interpretation of timestamps. Imagine a user sitting in Prague (Central European Time, CET) planning a trip to Miami, Florida (Eastern Standard Time, EST). If they view the weather details for Miami in the app and see a hurricane warning valid "until 22:00", which time zone is this timestamp in?

Displaying it in the user's local time (CET) is confusing because the user is trying to understand the situation at the destination. Displaying it in UTC is useless for the average person. The correct solution is to display the time in the local time zone of the location for which the alert is issued.

When processing the coordinates of the requested location, the application first determines the corresponding IANA time zone (e.g., America/New_York). Although alert validity timestamps are stored internally in UTC, they are transformed into the target location's time zone when formatted for the user interface. A user in Prague thus sees that the Miami alert is valid until 16:00 local time (EDT), which perfectly matches the actual situation on the ground.

When the Alert Is in a Foreign Language

Official alerts are primarily issued by national authorities in the official language of that country. The NWS publishes alerts in English, Spain's AEMET in Spanish, and CHMI in Czech. For a global platform, this presents a serious UX problem. If a user from the Czech Republic is traveling in Spain and receives a critical flash flood warning, they need to understand the instructions immediately, without having to copy texts into external translation tools.

To solve this problem, an on-demand AI translation system was integrated into the platform. Since translations generated by large language models incur costs, the entire process is subject to strict optimization:

  1. On-demand execution: Translation of the alert text (headline, description, and instructions) is not triggered automatically when the alert is downloaded to the server. It is only activated when a specific user requests the alert details in a language that does not match the original language of the message.
  2. Result caching: Once a translation is generated for a given combination of alert ID and target language, it is stored in the database. All subsequent users who view the same alert in the same language receive the cached result. Since the same warning is often read by thousands of people in a given area, the efficiency of this cache is extremely high.
  3. Gemini integration: The Gemini API is used for the translation itself. The model is provided with context indicating that the text is a weather warning, which ensures accurate preservation of technical terminology and the tone of the instructions.
  4. Graceful degradation: If the translation API fails (e.g., due to rate limits or service downtime), the application must not crash. In such cases, it degrades gracefully, displaying the original text in its native language to the user along with a note that the translation is temporarily unavailable. If an unexpected error occurs during the translation process, the internal quota management system ensures that the failed attempt does not count against consumption limits.

It is important to emphasize that at the current stage of development, this only involves the passive display and translation of official alerts issued by government authorities. User-defined weather monitors (e.g., "alert me if wind speed in my location exceeds 15 m/s"), which would require continuous background evaluation of personalized rules, are currently planned for the future and are not implemented in the system.


Robust integration of official weather alerts in the application shows that even a seemingly simple integration of public data requires a well-thought-out architecture. Using the CAP standard, geographic gates, port-based design, and efficient translation caching allows the platform to scale and easily expand to include other global sources. If you are interested in how these principles are applied in practice, visit the page describing the entire Weather InTouch project.

Related articles

Contact Info

Feel free to reach out for project inquiries, consulting, or longer-term collaboration. I'm open to discussing practical product work and frontend engineering opportunities.

Get in Touch

© 2026 Martin Hubalek. All rights reserved.