Skip to content

Eagle Notify

Daniel Truong edited this page Oct 13, 2026 · 1 revision

Eagle Notify

How EPIC sends project update emails. The service is eagle-notify. Citizens subscribe to project updates by email and manage their subscriptions from links in those emails. Staff sign in with IDIR through Keycloak to manage email templates and campaigns.

Local setup, publishing steps and the status of each feature are in the repo: README and docs/STATUS.md. This page covers how the service fits into EPIC.

Flow

flowchart LR
    Citizen["Citizen"] -->|"email links"| Pages["Storage static site<br/>React app"]
    Citizen -->|"subscribe form on project pages"| API
    Pages --> API["notify-api-&lt;env&gt;<br/>Azure Functions"]
    Staff["Staff (IDIR)"] --> Pages
    Staff -.->|"sign in"| KC["Keycloak<br/>realm eao-epic"]
    DEMI["demi-api"] -->|"POST /api/events"| API
    API --> Cosmos[("Cosmos DB<br/>NoSQL")]
    API --> ACS["Azure Communication<br/>Services Email"]
    ACS -->|"email"| Citizen
    ACS -.->|"delivery reports<br/>(Event Grid)"| API
Loading

The service has two parts on two hostnames: a static React site and a Function App. No Front Door route puts both on one origin, so the API keeps a CORS allow-list of the frontends that call it (frontendHostNames in azure/main.test.bicepparam).

Parts

Part Where What it is
API api/ Azure Functions v4, Node 22. HTTP routes for the subscriber flow, event ingest, health and staff screens. An Event Grid trigger handles delivery reports. Two timers send mail.
Web web/ React 19 and Vite 8, built with Node 24. Public subscribe and manage screens plus the staff screens. Its own package, not part of the root Yarn workspace.
Infrastructure azure/ Bicep: Cosmos account, Function App, static site, identity. Email is a separate deployment, azure/email.bicep.
CI and deploy .github/workflows/ pr.yaml and deploy-staging.yaml.
Decisions docs/adr/ Architecture decision records.

How it works

Subscribers never sign in. A subscriber proves who they are by holding a code that was emailed to them. The code is in the link, so confirm, manage, unsubscribe and undo all work from the email. Confirming a subscription records consent. Unconfirmed and deleted rows expire through Cosmos TTL, not a scheduled job.

Staff sign in with Keycloak. The React app starts the IDIR sign-in only on the staff routes. The API checks every staff request itself (api/src/auth.js): it verifies the token signature against the realm keys, then checks the issuer, the client the token was issued to, and a staff role. See Keycloak Configuration for the realm.

Mail goes out in two ways.

  • Campaigns: staff write an email template and start a campaign. A timer (campaignSlice) runs every 5 minutes, except during the 14:00 UTC hour, and sends the campaign in slices.
  • Daily digest: a timer (dailyDigest) runs at 14:00 UTC. It sends each subscriber one message that covers the previous UTC day's events for every service they follow.

Events come from DEMI. When an update is published, demi-api posts it to POST /api/events with a function key. The payload and retry rules are in Eagle DEMI.

Staff edit emails in a block editor in the browser. The server stores MJML and compiles it to HTML, with Handlebars filling in the variables. The plain-text part is derived from that HTML. Nothing the browser produces is sent as is.

Azure Communication Services reports delivery results through Event Grid. The API records bounces against the subscriber.

Web app

  • React 19 and Vite 8, with react-router 7 in hash-router mode. The hash router is deliberate: the static site has no rewrite rule, and the subscriber code in an email link sits after the #, so it never reaches a server log.
  • Staff screens are lazy-loaded, so a citizen who opens an unsubscribe link downloads no staff code.
  • TanStack Query holds staff data.
  • Styling vendors the eagle-design-kit CSS. The header and footer follow the design shared with eagle-public, with Notify's own name in the header. Screens change with no animation.

Environments

Environment State
test (staging) API at notify-api-test.azurewebsites.net. Pages on the storage account's static site. Cosmos account notify-cosmos-test. Email through an Azure Communication Services resource.
prod None.

Staging is spelled test in Azure and in the GitHub environment name.

Deploy

Run the Deploy to staging workflow (deploy-staging.yaml) from the Actions tab of the repo. It has a manual trigger and uses the GitHub environment test. The job:

  1. Builds the web app and the API package.
  2. Logs in to Azure with OIDC, using the CI identity created by scripts/bootstrap-cicd.sh.
  3. Deploys the API zip and waits for the deployment to succeed.
  4. Checks that /api/health returns 200.
  5. Saves a copy of the $web container as the web-rollback-<run id> artifact (kept 30 days).
  6. Uploads the pages, index.html last, then checks the site serves the new entry file.

To roll back the pages, put the old index.html back from the saved copy. Infrastructure is deployed by hand with Bicep; the steps are in the README under Publishing. Every PR runs lint, tests, the Bicep compile and a contrast check (pr.yaml).

Where to look

Question Look at
What is built, what is blocked docs/STATUS.md
Why Functions and Cosmos, and why not listmonk docs/adr/0001-runtime-and-datastore.md
Subscriber routes api/functions/subscriptions.js
Staff routes api/functions/staff.js
Event ingest and daily digest api/functions/events.js
Staff token checks api/src/auth.js
Bicep azure/main.bicep, azure/modules/
Web routes web/src/router.tsx

Related

Clone this wiki locally