- Python 80.1%
- HTML 15.6%
- CSS 3.8%
- JavaScript 0.4%
| .claude | ||
| alembic | ||
| app | ||
| docs | ||
| static | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| alembic.ini | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pytest.ini | ||
| README.md | ||
| requirements-test.txt | ||
| requirements.txt | ||
| SECURITY_AUDIT_REMEDIATION_PLAN.md | ||
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_PORTin.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=developmentSESSION_HTTPS_ONLY=falseAUTH_REDIRECT_URI=http://localhost:8000/auth/callback
For production, set:
APP_ENV=productionSESSION_HTTPS_ONLY=trueAUTHENTIK_URLandAUTH_REDIRECT_URItohttps://...values- a strong
SECRET_KEYof at least 32 characters AUTHENTIK_URLto the Authentik base URL only, not a provider path such as/application/o/...
2. Configure Authentik
In your Authentik admin panel:
- Create an OAuth2 / OpenID Connect provider
- Set the redirect URI to
http://your-host:8000/auth/callbackfor local development, orhttps://your-host/auth/callbackin production - Create an Application pointing to that provider
- Create two groups (names are configurable in
.env):weatherbot-admins— full admin accessweatherbot-users— standard user access- Users in neither group get read-only access
- Copy the Client ID and Client Secret into
.env - If your Authentik provider's issuer URL ends in a provider slug that is different from the OAuth client ID, also set
AUTHENTIK_ISSUERto the issuer shown by Authentik, for examplehttps://auth.example.com/application/o/weatherbot/
Notes:
AUTHENTIK_URLshould be the Authentik site root such ashttps://auth.example.comAUTHENTIK_ISSUERshould 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
Security-related settings
APP_ENVdefaults todevelopment. Set it toproductionorstagingto enable stricter startup validation.SESSION_HTTPS_ONLYcontrols whether session and CSRF cookies use theSecureflag. It now defaults totruein production andfalsein development.SESSION_SAME_SITEcontrols the SameSite policy for session and CSRF cookies. Supported values arelax,strict, andnone.AUTH_REDIRECT_URImust match your Authentik application configuration. In production it must usehttps://.AUTHENTIK_ISSUERis optional. Set it when Authentik's issuer URL is not exactlyAUTHENTIK_URL + /application/o/ + AUTHENTIK_CLIENT_ID + /.AUTHENTIK_URLshould 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_KEYis weak or shorter than 32 charactersSESSION_HTTPS_ONLYis disabled- required OIDC settings are missing
AUTHENTIK_URLorAUTH_REDIRECT_URIusehttp://instead ofhttps://
| 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
- Go to Locations → + Add Location
- Search by city name or address, then click or drag the pin to fine-tune
- Set a minimum severity threshold — alerts below this level are ignored for this location
- 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, orWarning) - 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:
- Select a location from the dropdown
- Optionally override the minimum severity for this specific pairing (leave as "Default" to use the location's own threshold)
- 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
- 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
- APScheduler calls
GET /alerts/active?area=STATE1,STATE2every 60 seconds (one request covers all monitored states) - Each alert's
affectedZonesis checked against the NWS zone IDs stored for each location - Matching alerts are checked against the
sent_alertstable for deduplication - 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
- Are recorded in
Live dashboard updates
- Browser connects to
GET /sse(FastAPIStreamingResponse) - Server does
LISTEN weatherbot_eventson a dedicated asyncpg connection - Alert processor's
pg_notifyputs a message in the queue - SSE handler pushes the event to the browser
- 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.