A bot that warns of severe weather in configurable locations
  • Python 80.1%
  • HTML 15.6%
  • CSS 3.8%
  • JavaScript 0.4%
Find a file
2026-05-13 14:20:37 -05:00
.claude Updated SPC roadmap. 2026-04-20 14:49:05 -05:00
alembic Add freshness history and cache metrics 2026-05-13 14:20:37 -05:00
app Add freshness history and cache metrics 2026-05-13 14:20:37 -05:00
docs Add freshness history and cache metrics 2026-05-13 14:20:37 -05:00
static Add freshness history and cache metrics 2026-05-13 14:20:37 -05:00
tests Add freshness history and cache metrics 2026-05-13 14:20:37 -05:00
.env.example Add SPC outlook detail maps and dashboard visibility 2026-04-26 21:26:11 -05:00
.gitignore Initial implementation of WeatherBot 2026-03-26 16:13:34 -05:00
alembic.ini Initial implementation of WeatherBot 2026-03-26 16:13:34 -05:00
CHANGELOG.md Add CHANGELOG.md 2026-04-25 19:34:26 -05:00
CLAUDE.md Add CLAUDE.md with architecture guidance for Claude Code 2026-03-27 00:28:38 -05:00
docker-compose.yml Initial implementation of WeatherBot 2026-03-26 16:13:34 -05:00
Dockerfile Initial implementation of WeatherBot 2026-03-26 16:13:34 -05:00
pytest.ini Tests 2026-04-06 14:30:03 -05:00
README.md Add SPC outlook detail maps and dashboard visibility 2026-04-26 21:26:11 -05:00
requirements-test.txt Tests 2026-04-06 14:30:03 -05:00
requirements.txt Initial implementation of WeatherBot 2026-03-26 16:13:34 -05:00
SECURITY_AUDIT_REMEDIATION_PLAN.md Harden auth, authorization, CSRF, config, and webhook security 2026-04-09 13:41:35 -05:00

WeatherBot

Self-hosted weather monitoring bot. Watches an arbitrary number of US locations via the National Weather Service (NWS) API and delivers alerts to Discord, Signal, Matrix, webhooks, Pushover, and SMS via Twilio or Voip.ms. A FastAPI web app provides a live-updating dashboard and configuration UI — no page reloads required.


Features

  • Real-time NWS alerts — polls active watches, warnings, and advisories every 60 seconds, batched across all monitored states in a single API call
  • Per-location alert routing — each location independently routes to any set of notification channels
  • Per-location event filtering — exclude alert types you don't care about (e.g. Flood, Dense Fog) on a location-by-location basis
  • Multi-channel notifications: Discord, Signal, Matrix, Webhooks, Pushover, SMS via Twilio or Voip.ms (all optional)
  • Radar map — interactive Leaflet map with NWS CONUS WMS overlay; switch between Base Reflectivity, Composite Reflectivity, Echo Tops, and Precipitation Type
  • Forecast modal — tabbed view with 7-day forecast, Area Forecast Discussion (AFD), and Severe Weather Summary (active alerts + recent SPS/SVS statements, last 3 hours only)
  • Live dashboard — HTMX + Server-Sent Events push updates to the browser without page reload; location cards show current alert status (WARNING / WATCH / ADVISORY / OK)
  • Authentik OIDC login with three role levels: admin, user, read-only
  • Location sharing — admins can share personal locations with specific users by email
  • Leaflet.js map picker with Nominatim geocoding for adding locations
  • Public per-location pages (no login required, opt-in per location)
  • Configurable host port via APP_PORT in .env

Quick Start

1. Copy and configure environment

cp .env.example .env

Edit .env. At minimum you must set:

Variable What to put
POSTGRES_PASSWORD Any strong password
SECRET_KEY Output of openssl rand -hex 32
NWS_USER_AGENT e.g. WeatherBot/1.0 you@example.com (required by NWS API)
AUTHENTIK_URL Base URL of your Authentik instance
AUTHENTIK_CLIENT_ID OAuth2 client ID from Authentik
AUTHENTIK_CLIENT_SECRET OAuth2 client secret from Authentik

For local development, keep:

  • APP_ENV=development
  • SESSION_HTTPS_ONLY=false
  • AUTH_REDIRECT_URI=http://localhost:8000/auth/callback

For production, set:

  • APP_ENV=production
  • SESSION_HTTPS_ONLY=true
  • AUTHENTIK_URL and AUTH_REDIRECT_URI to https://... values
  • a strong SECRET_KEY of at least 32 characters
  • AUTHENTIK_URL to the Authentik base URL only, not a provider path such as /application/o/...

2. Configure Authentik

In your Authentik admin panel:

  1. Create an OAuth2 / OpenID Connect provider
  2. Set the redirect URI to http://your-host:8000/auth/callback for local development, or https://your-host/auth/callback in production
  3. Create an Application pointing to that provider
  4. Create two groups (names are configurable in .env):
    • weatherbot-admins — full admin access
    • weatherbot-users — standard user access
    • Users in neither group get read-only access
  5. Copy the Client ID and Client Secret into .env
  6. If your Authentik provider's issuer URL ends in a provider slug that is different from the OAuth client ID, also set AUTHENTIK_ISSUER to the issuer shown by Authentik, for example https://auth.example.com/application/o/weatherbot/

Notes:

  • AUTHENTIK_URL should be the Authentik site root such as https://auth.example.com
  • AUTHENTIK_ISSUER should be the full OIDC issuer URL for the provider when needed

3. Start services

docker compose up -d

Database migrations run automatically on startup. The app will be available at http://localhost:8000 (or whatever APP_PORT is set to).

4. Register Signal (one-time, if using Signal)

Signal requires linking to a phone number before it can send messages. This is a one-time step:

docker compose exec signal-cli signal-cli link -n "WeatherBot"

Open the printed URL on your phone under Settings → Linked Devices. Then in .env:

SIGNAL_ENABLED=true
SIGNAL_SENDER=+15555555555   # the number you linked

Restart the app: docker compose restart app

5. Access the webapp

Open http://localhost:8000, click Sign in with Authentik, and log in.


Updating

After pulling new code, rebuild and restart the app container:

docker compose up -d --build app

Database migrations run automatically on startup.


Environment Variables

  • APP_ENV defaults to development. Set it to production or staging to enable stricter startup validation.
  • SESSION_HTTPS_ONLY controls whether session and CSRF cookies use the Secure flag. It now defaults to true in production and false in development.
  • SESSION_SAME_SITE controls the SameSite policy for session and CSRF cookies. Supported values are lax, strict, and none.
  • AUTH_REDIRECT_URI must match your Authentik application configuration. In production it must use https://.
  • AUTHENTIK_ISSUER is optional. Set it when Authentik's issuer URL is not exactly AUTHENTIK_URL + /application/o/ + AUTHENTIK_CLIENT_ID + /.
  • AUTHENTIK_URL should remain the base Authentik URL. Do not include /application/o/... in this setting.

Production Startup Validation

When APP_ENV is production or staging, WeatherBot now fails fast on insecure configuration. Startup will be blocked if:

  • SECRET_KEY is weak or shorter than 32 characters
  • SESSION_HTTPS_ONLY is disabled
  • required OIDC settings are missing
  • AUTHENTIK_URL or AUTH_REDIRECT_URI use http:// instead of https://
Variable Required Default Description
APP_PORT 8000 Host port the app is exposed on
APP_BASE_URL Public root URL for this WeatherBot instance; used for unauthenticated media URLs such as Webex SPC outlook GIFs
POSTGRES_PASSWORD PostgreSQL password
SECRET_KEY Session signing key (openssl rand -hex 32)
NWS_USER_AGENT NWS API User-Agent header (must include contact email)
AUTHENTIK_URL Base URL of your Authentik instance
AUTHENTIK_CLIENT_ID OAuth2 client ID
AUTHENTIK_CLIENT_SECRET OAuth2 client secret
AUTHENTIK_ISSUER derived from AUTHENTIK_URL + AUTHENTIK_CLIENT_ID Explicit OIDC issuer URL when Authentik uses a provider slug/path that differs from the client ID
AUTHENTIK_ADMIN_GROUP weatherbot-admins Authentik group name that grants admin role
AUTHENTIK_USER_GROUP weatherbot-users Authentik group name that grants user role
AUTH_REDIRECT_URI http://localhost:8000/auth/callback Must match the redirect URI registered in Authentik
DISCORD_BOT_TOKEN Discord bot token; Discord disabled if blank
MATRIX_HOMESERVER Matrix homeserver URL
MATRIX_USER Matrix bot user ID (e.g. @weatherbot:example.com)
MATRIX_PASSWORD Matrix bot password
SIGNAL_ENABLED false Set true after completing Signal device linking
SIGNAL_SENDER Sender phone number in E.164 format
TWILIO_ACCOUNT_SID Twilio account SID; SMS disabled if blank
TWILIO_AUTH_TOKEN Twilio auth token
TWILIO_FROM_NUMBER Twilio sender number in E.164 format
VOIPMS_API_USERNAME Voip.ms API username (account email)
VOIPMS_API_PASSWORD Voip.ms API password set in the portal
VOIPMS_API_URL https://voip.ms/api/v1/rest.php Voip.ms REST endpoint
ALERT_POLL_INTERVAL 60 Alert polling interval in seconds
SPC_POLL_INTERVAL_MINUTES 5 SPC convective/fire/MCD polling interval in minutes
FORECAST_CACHE_MINUTES 30 How long to cache forecast data
RADAR_CACHE_MINUTES 5 How long to cache server-side radar images

User Roles

Roles are sourced from Authentik group membership and re-synced on every login.

Role Permissions
admin Create and manage global locations (visible to all users); manage all notification channels; change location visibility and sharing; access all dashboards
user Create and manage personal locations (visible only to themselves and admins); create personal notification channels; view all dashboards
readonly View dashboards and alert history only; no create/edit/delete

Locations

Adding a location

  1. Go to Locations+ Add Location
  2. Search by city name or address, then click or drag the pin to fine-tune
  3. Set a minimum severity threshold — alerts below this level are ignored for this location
  4. Click Add Location

After saving, the app resolves NWS data in the background (WFO, forecast grid, alert zone, nearest radar station). The location card shows a spinner until this completes. If resolution fails, a Retry button appears.

Editing a location

Click Edit on any location row to open the edit modal. Available fields:

  • Name — display name
  • Visibility (admin only) — see Location Visibility below
  • Min Severity — minimum alert level to process for this location (Advisory, Watch, or Warning)
  • Excluded alert keywords — comma-separated keywords; any alert whose event name contains a match is silently ignored for this location (e.g. Flood, Dense Fog, Rip Current). Case-insensitive substring match.
  • Enable public page — enables a login-free public page at /api/locations/{id}/public

Location visibility

Admins can set visibility on any location:

Visibility Who can see it
Global All logged-in users
Personal — shared explicitly Only the admin (owner) plus any users explicitly added

When set to Personal, a Shared With section appears in the edit modal. Enter a registered user's email address and click Add to grant them access. Click on a chip to revoke it. Changes are immediate — no need to save separately.

Shared users see the location on their dashboard and can subscribe their notification channels to it, but cannot edit or delete it.


Dashboard

The dashboard shows all locations visible to the current user and a live feed of active alerts.

Location cards

Each card displays:

  • Location name
  • Alert status badge — reflects current active alerts in the database:
    • 🔴 WARNING — an active warning is in effect
    • 🟠 WATCH — an active watch is in effect
    • 🟡 ADVISORY — an active advisory is in effect
    • 🟢 OK — no active alerts
  • Zone, state, and radar station identifiers
  • Forecast and Radar buttons

Forecast modal

Clicking Forecast opens a tabbed modal:

Tab Content
Forecast 7-day period forecast with temperature, wind, and detailed description
Discussion Latest Area Forecast Discussion (AFD) from the location's Weather Forecast Office — the meteorologist's written analysis
Severe Summary Active NWS alerts affecting the location's zones, plus Special Weather Statements and Severe Weather Statements from the WFO issued in the last 3 hours

Radar modal

Clicking Radar opens an interactive Leaflet map centered on the location with live NWS radar data overlaid. Use the layer buttons to switch products:

Layer Description
Base Reflectivity Standard NEXRAD base radar (default)
Composite Reflectivity Maximum reflectivity across all elevation scans
Echo Tops Height of radar returns — useful for estimating storm depth
Precip Type Precipitation type classification (rain, snow, mix, etc.)

Radar data is sourced from the NWS CONUS GeoServer WMS. The map supports full zoom with no tile level restrictions.


Notification Channels

Creating a channel

Go to Channels+ Add Channel. Choose a type and fill in the type-specific config fields. All channels have an Animated radar GIF toggle (used when the channel sends radar images with alerts).

Channel types

Discord

guild_id:    Server (guild) ID — right-click the server icon → Copy Server ID
channel_id:  Text channel ID — right-click the channel → Copy Channel ID

The Discord bot must be invited to the server with permission to send messages and attach files. Bot token goes in DISCORD_BOT_TOKEN.

Slash commands available in Discord:

  • /weather <location> — current forecast for a monitored location
  • /radar <location> — radar image
  • /alerts — list of active alerts across all locations

Signal

recipients:  List of phone numbers in E.164 format (e.g. +15555555555)

Requires the one-time Signal device linking step (see Quick Start).

Matrix

room_id:  Matrix room ID (e.g. !abc123:example.com)

The bot must be invited to the room. Credentials go in MATRIX_* env vars.

Commands available in Matrix:

  • !weather <location>
  • !radar <location>
  • !alerts

Webhook

url:     Endpoint URL to POST/PUT to
method:  POST or PUT

The webhook receives a JSON payload with event, severity, headline, location, and expires fields.

SMS

provider:     twilio or voipms
to_number:    Recipient phone number
from_number:  Voip.ms DID to send from (Voip.ms only)

Messages are plain text (no images).

For Twilio, set TWILIO_* in .env.

For Voip.ms, set VOIPMS_* in .env, enable API access in the Voip.ms portal, and whitelist the IP address of the WeatherBot server. Voip.ms documents API SMS via its sendSMS method and notes that A2P/business texting may require separate verification depending on your usage.

Webex

bot_token:  Bot token from developer.webex.com/my-apps
room_id:    Webex space/room ID to post to

Create a Bot at developer.webex.com, add it to the target Webex space, then copy the bot token and the space's Room ID (available via the Webex API or the developer portal). Alerts are sent as markdown messages with the Iowa State warning polygon image attached inline. All-clears include the expiry image with the "Event No Longer Active" overlay. For inline SPC animated outlook GIFs, set APP_BASE_URL to the public HTTPS root of this WeatherBot instance so Webex can fetch /media/spc-outlook/...gif.

Pushover

user_key:   User key from pushover.net/settings
app_token:  Application API token from pushover.net/apps
priority:   -2 (silent) to 2 (emergency with acknowledgment)

Priority levels:

Value Behavior
-2 No notification sound or alert
-1 Quiet — delivered silently
0 Normal priority
1 High priority — bypasses quiet hours
2 Emergency — repeats every 60s until acknowledged; requires acknowledgment

Radar images can be attached (up to 2.5 MB; Pushover supports image attachments).

Subscribing a location to a channel

On the Channels page, each channel card has a subscription form:

  1. Select a location from the dropdown
  2. Optionally override the minimum severity for this specific pairing (leave as "Default" to use the location's own threshold)
  3. Optionally configure:
    • Daily forecast time — one digest per local day at that location's local time
    • Quiet hours start/end — suppress non-bypass notifications during the local quiet window
    • SPC Outlooks, MCDs, and Fire — opt in to SPC-derived notifications for this subscription
  4. Click Add

Subscribed locations appear as a list on the channel card. Click to unsubscribe.

A location with no subscriptions is dashboard-only — alerts appear on the web UI but are not sent anywhere.

Subscription edit modal controls currently include:

  • Min Severity
  • Daily Forecast Time
  • Quiet Hours Start / End
  • SPC Convective Outlooks
  • Mesoscale Discussions (coarse state match in v1)
  • SPC Fire Outlooks

Alert Filtering

Minimum severity threshold

Each location has a Min Severity setting controlling the lowest level of alert it processes:

Setting What gets through
Advisory Advisories, watches, and warnings
Watch Watches and warnings only
Warning Warnings only

Channel subscriptions can further override this threshold for a specific location/channel pair.

Excluded event keywords

To ignore specific alert types at a location (e.g. you don't need flood alerts for a hilltop location), add keywords in the location's Edit modal under Excluded alert keywords.

Enter comma-separated keywords. Any NWS alert whose event name contains one of the keywords (case-insensitive substring match) will be skipped entirely for that location — it won't appear on the dashboard or be sent to any channel.

Examples:

Keywords What gets filtered
Flood Flood Watch, Flood Warning, Flash Flood Watch, Flash Flood Warning, …
Dense Fog Dense Fog Advisory
Rip Current Rip Current Statement
Flood, Dense Fog Both of the above

Notification Policy

Subscription-level controls now apply beyond basic NWS alerts:

  • Quiet hours suppress NWS watches/advisories, SPC outlooks, SPC discussions, forecast digests, and non-warning lifted notifications
  • NWS warnings bypass quiet hours
  • Forecast digests send at most once per subscription per local day after the configured local send time
  • Lifted / all-clear notifications are tracked separately from alert clearing so quiet-hour deferrals can retry later without duplicating sends

Current implementation note:

  • notifier code still uses send_all_clear() naming internally even though the lifecycle behavior now maps more closely to a dedicated lifted-notification flow

Architecture

docker compose
  ├── app          FastAPI + APScheduler + Discord bot + Matrix client (port 8000)
  ├── db           PostgreSQL 16
  └── signal-cli   bbernhard/signal-cli-rest-api

Alert pipeline

  1. APScheduler calls GET /alerts/active?area=STATE1,STATE2 every 60 seconds (one request covers all monitored states)
  2. Each alert's affectedZones is checked against the NWS zone IDs stored for each location
  3. Matching alerts are checked against the sent_alerts table for deduplication
  4. New alerts that pass the severity threshold and keyword filters:
    • Are recorded in sent_alerts
    • Trigger pg_notify('weatherbot_events', ...) for the SSE pipeline
    • Are dispatched to each subscribed notification channel

Live dashboard updates

  1. Browser connects to GET /sse (FastAPI StreamingResponse)
  2. Server does LISTEN weatherbot_events on a dedicated asyncpg connection
  3. Alert processor's pg_notify puts a message in the queue
  4. SSE handler pushes the event to the browser
  5. HTMX SSE extension swaps the affected DOM fragments — no page reload

NWS data resolution

When a location is created, a background task calls GET /points/{lat},{lon} to resolve:

  • Weather Forecast Office (WFO) code
  • Forecast grid coordinates (for forecast and hourly forecast)
  • Forecast zone ID and county zone ID (for alert matching)
  • Nearest radar station
  • State abbreviation (for batching alert queries)

This data is stored on the location record and never re-fetched (unless you use Retry after an error).


Development

# Install dependencies
pip install -r requirements.txt

# Copy and configure env
cp .env.example .env

# Run migrations (requires a running PostgreSQL)
alembic upgrade head

# Start dev server with hot reload
uvicorn app.main:app --reload

The app expects DATABASE_URL to point to a reachable PostgreSQL 16 instance. All other services (Discord, Matrix, Signal) are optional and disabled if the relevant env vars are not set.

Database migrations

Migrations live in alembic/versions/. To create a new one after changing a model:

alembic revision --autogenerate -m "describe your change"
alembic upgrade head

Migrations run automatically on Docker startup via the app's lifespan handler.