Skip to content

Latest commit

 

History

History
327 lines (245 loc) · 12.6 KB

File metadata and controls

327 lines (245 loc) · 12.6 KB

Avalanche Canada SMS Service — Technical Specification

Overview

A serverless application that accepts incoming SMS messages containing latitude/longitude coordinates, looks up the corresponding Avalanche Canada forecast region, and replies with a template-formatted summary of current avalanche conditions. Includes an admin web UI for monitoring.

Core Workflow

  1. User sends SMS to a Twilio phone number with a message containing coordinates (e.g. 51.0447 -115.0632)
  2. Twilio forwards the message to the server via webhook (HTTP POST)
  3. Server parses lat/long from the message body
  4. Server queries Avalanche Canada API to determine which forecast region contains the coordinates
  5. Server fetches the current forecast bulletin for that region
  6. Server formats the forecast into an SMS-friendly summary using a template
  7. Server replies via Twilio with the summary SMS
  8. Request metadata is logged in-memory for the admin dashboard

Technology Stack

Component Choice
Runtime Node.js with TypeScript
HTTP Framework Hono
SMS Provider Twilio
Forecast Source Avalanche Canada public API
Summarization Template-based (no LLM)
Deployment Target Cloudflare Workers or AWS Lambda
Storage In-memory (no database)
Caching None

API Endpoints

POST /sms/incoming

Twilio webhook endpoint for incoming SMS messages.

Request: Twilio webhook form-encoded POST body containing Body, From, To, MessageSid, etc.

Behavior:

  1. Validate the request is from Twilio (signature verification)
  2. Parse coordinates from Body field
  3. Look up forecast region and fetch bulletin
  4. Format response using template
  5. Return TwiML XML response with reply message

Response: Content-Type: text/xml — TwiML <Response><Message>...</Message></Response>

Error cases:

  • Unparseable coordinates → reply with usage instructions
  • Coordinates outside any forecast region → reply stating no forecast region found
  • Avalanche Canada API unavailable → reply with error message asking to try later

GET /admin

Serves the admin dashboard HTML page.

GET /admin/status

Returns live health check results for all dependent services.

Response:

{
  "overall": "ok",
  "checked_at": "2026-02-23T10:30:00Z",
  "services": [
    {
      "name": "Avalanche Canada API",
      "status": "ok",
      "latency_ms": 142,
      "detail": "Responding normally"
    },
    {
      "name": "Twilio",
      "status": "ok",
      "latency_ms": 87,
      "detail": "Authenticated and responding"
    }
  ]
}

overall is "ok" if all services are ok, "degraded" if any are degraded, "down" if any are down.

GET /health

Returns 200 OK with { "status": "ok" } for uptime monitoring.

Coordinate Parsing

The server must parse coordinates from free-text SMS messages. Supported input formats:

Raw Coordinate Formats

Format Example
Decimal degrees 51.0447 -115.0632
Comma-separated 51.0447, -115.0632
With cardinal directions 51.0447N 115.0632W
Signed (negative for W/S) 51.0447 -115.0632

Map Link Formats (Mobile Location Sharing)

When users share their location from a phone, the SMS body contains a URL rather than raw coordinates. The server must detect and extract coordinates from these URL formats.

Apple Maps (iOS "Share My Location" / "Send My Current Location"):

URL Pattern Example
maps.apple.com/?ll={lat},{lng} https://maps.apple.com/?ll=51.0447,-115.0632&q=...
maps.apple.com/?q={lat},{lng} https://maps.apple.com/?q=51.0447,-115.0632
maps.apple.com with address param Extract ll param if present, else geocode not supported

Parse the ll query parameter first (takes precedence). Fall back to q if ll is absent and q contains a coordinate pair.

Google Maps (Android share via Google Maps / Google Messages):

URL Pattern Example
google.com/maps with @{lat},{lng} in path https://www.google.com/maps/@51.0447,-115.0632,15z
google.com/maps with q={lat},{lng} https://www.google.com/maps?q=51.0447,-115.0632
maps.app.goo.gl/{id} (shortened) https://maps.app.goo.gl/abc123
goo.gl/maps/{id} (legacy shortened) https://goo.gl/maps/abc123

For shortened URLs (maps.app.goo.gl, goo.gl/maps): the server must follow the redirect (HTTP HEAD or GET) to resolve the full URL, then extract coordinates from the expanded URL using the patterns above.

Parsing Strategy

  1. Check if the message body contains a URL (starts with http or contains a known maps domain)
  2. If URL found, match against known map link patterns and extract coordinates
  3. If no URL, attempt to parse raw coordinate formats
  4. Validate extracted coordinates against Canadian bounds

Validation rules:

  • Latitude must be between 40.0 and 70.0 (reasonable for Canada)
  • Longitude must be between -145.0 and -50.0 (reasonable for Canada)
  • If coordinates are outside Canada's bounds, return an error message

Avalanche Canada API Integration

Region Lookup

Endpoint: GET https://api.avalanche.ca/forecasts/en/areas

Returns GeoJSON FeatureCollection of all forecast regions with polygon boundaries. Use point-in-polygon testing to determine which region contains the user's coordinates.

The region polygons should be fetched on server startup (or first request) and held in memory for point-in-polygon lookups. Re-fetch periodically (e.g. daily) to pick up boundary changes.

Forecast Fetch

Endpoint: GET https://api.avalanche.ca/forecasts/en/products/point?lat={lat}&long={lng}

Returns the forecast bulletin for the region containing the given point. This is the only endpoint needed — it handles region lookup internally. Note the query parameter is long, not lng.

Forecast Data Structure (Actual API Response)

The response is a single JSON object with this structure:

{
  report: {
    title: string              // Region name (e.g. "Waterton")
    dateIssued: string         // ISO date
    validUntil: string         // ISO date
    highlights: string         // HTML summary text
    dangerRatings: [{
      date: { value: string, display: string },
      ratings: {
        alp: { display: "Alpine", rating: { value: string, display: string } },
        tln: { display: "Treeline", rating: { value: string, display: string } },
        btl: { display: "Below Treeline", rating: { value: string, display: string } }
      }
    }],
    problems: [{
      type: { value: string, display: string },  // e.g. "Wind slab"
      ...
    }]
  }
}

Danger rating value field uses: "low", "moderate", "considerable", "high", "extreme", "noRating".

SMS Response Template

The reply must fit within a single SMS: 160 characters maximum (GSM-7 encoding). This keeps costs to one segment per reply and forces a concise, scannable format.

Template

{region_name}
{date_issued}
Alp:{alpine} TL:{treeline} BT:{below_treeline}
{problem_list}
avalanche.ca

Example (137 chars):

Kananaskis
Feb 23
Alp:Considerable TL:Moderate BT:Low
Storm Slabs, Wind Slabs
avalanche.ca

Fitting Within 160 Characters

  • Use abbreviated elevation labels: Alp, TL, BT
  • Use short date format: Mon DD (e.g. Feb 23)
  • Omit "Avalanche Forecast" header text
  • Omit highlights/summary text
  • If problem list would push over 160 chars, truncate to first N problems that fit
  • If the region name is very long, truncate with ellipsis to keep total under 160

Danger Rating Display

Map numeric/enum ratings to display strings:

  • 1Low
  • 2Moderate
  • 3Considerable
  • 4High
  • 5Extreme
  • No rating → No Rating

Admin Dashboard

A single-page HTML status dashboard served at /admin. Uses inline CSS and vanilla JS (no build step, no framework). Fetches data from /admin/status on load and via polling every 30 seconds.

Dashboard Content

  • Overall status banner — green (all ok), yellow (degraded), red (down)
  • Service cards — one per dependency, showing name, status badge, detail message, and response latency
  • Refresh button — manual re-check
  • Last checked timestamp

Health Checks

Each check runs live on every /admin/status request:

Service Check Method OK Condition
Avalanche Canada API GET /forecasts/en/products/point with a test coordinate HTTP 200 or 404 (API up, just no region at test point)
Twilio GET /2010-04-01/Accounts/{sid}.json with Basic auth HTTP 200 (credentials valid)

Twilio Configuration

Webhook Setup

The Twilio phone number's incoming message webhook must be configured to POST to https://{host}/sms/incoming.

Request Validation

All incoming requests to /sms/incoming must be validated using Twilio's request signature verification to prevent spoofing. The X-Twilio-Signature header is validated against the auth token.

In development/testing, signature validation can be disabled via environment variable.

Environment Variables

Variable Required Description
TWILIO_ACCOUNT_SID Yes Twilio account SID
TWILIO_AUTH_TOKEN Yes Twilio auth token for signature validation
TWILIO_PHONE_NUMBER No The Twilio phone number (for display purposes)
ADMIN_ENABLED No Set to false to disable admin routes (default: true)
VALIDATE_TWILIO_SIG No Set to false to skip signature validation (dev only)

Error Handling

All errors must be caught and result in a friendly SMS reply. The user should never receive a raw error or no response.

Scenario SMS Reply
Unparseable message "Send your coordinates as: lat, lng (e.g. 51.04, -115.06)"
Coordinates outside Canada "Those coordinates are outside Canadian avalanche forecast areas."
No forecast region for location "No avalanche forecast region covers that location."
Avalanche.ca API error "Unable to fetch forecast right now. Please try again later."
No active forecast for region "No active forecast for {region}. Check avalanche.ca for updates."

Project Structure

/
├── src/
│   ├── index.ts              # Hono app setup, route registration
│   ├── routes/
│   │   ├── sms.ts            # POST /sms/incoming handler
│   │   ├── admin.ts          # GET /admin, GET /admin/status
│   │   └── health.ts         # GET /health
│   ├── services/
│   │   ├── avalanche-ca.ts   # Avalanche Canada API client
│   │   ├── twilio.ts         # Twilio signature validation, TwiML helpers
│   │   └── status.ts         # Live health checks for dependent services
│   ├── lib/
│   │   ├── parse-coordinates.ts   # Coordinate parsing from SMS text
│   │   └── format-forecast.ts     # Template formatting for SMS reply
│   └── templates/
│       └── admin.html        # Admin dashboard HTML
├── test/
│   ├── parse-coordinates.test.ts
│   ├── format-forecast.test.ts
│   ├── sms.test.ts
│   └── avalanche-ca.test.ts
├── wrangler.toml             # Cloudflare Workers config
├── tsconfig.json
├── package.json
├── CLAUDE.md
├── README.md
└── SPECIFICATION.md

Testing

  • Unit tests for coordinate parsing (all supported formats, edge cases, invalid input)
  • Unit tests for forecast template formatting
  • Integration tests for the SMS webhook handler using mock Avalanche.ca responses
  • Test runner: Vitest

Non-Goals

  • User authentication (the SMS endpoint is secured by Twilio signature verification only)
  • Persistent storage or analytics
  • Multi-language support (English only, matching avalanche.ca/en)
  • LLM-based summarization
  • Rate limiting (rely on Twilio's built-in protections)
  • Forecast caching