Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 84 additions & 97 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,147 +1,134 @@
# Notifications tooling monorepo
# Notifications tooling

## Install dev tools
A monorepo for Guardian notifications tooling.

The DevX tooling relies on [Bun](https://bun.com/)
It currently contains a single editorial tool called Dispatch (under development), used to compose and send notifications.

On Mac OS install its latest version using Homebrew:
## Contents

```sh
brew install bun
```

## Linting & formatting

This repo follows the [Guardian's linting recommendations](https://github.com/guardian/recommendations/blob/main/client-side.md#coding-style) using the shared department configs:

- [`@guardian/eslint-config`](https://github.com/guardian/csnx/tree/main/libs/%40guardian/eslint-config) for linting JavaScript, TypeScript and React (this bundles the TypeScript, React, hooks, a11y, imports and comments rule sets).
- [`@guardian/prettier`](https://github.com/guardian/csnx/tree/main/libs/%40guardian/prettier) for formatting.

ESLint is configured via [`eslint.config.js`](./eslint.config.js) (flat config). The React ruleset is scoped to the frontend app only.
- [Introduction](#1-introduction)
- [Getting Started](#2-getting-started)
- [How It Works](#3-how-it-works)
- [Useful Links](#4-useful-links)
- [Terminology](#5-terminology)

Available scripts (run from the repo root):
## 1. Introduction

```sh
# Lint everything
bun run lint
Dispatch gives editorial staff one place to compose and send breaking-news notifications.

# Lint and auto-fix where possible
bun run lint:fix
It replaces a fragmented workflow spread across multiple systems and legacy tooling.

# Format all files
bun run format
Email delivery currently integrates with [Braze](https://www.braze.com/docs/developer_guide/home), and app push integration with the [notifications API](https://github.com/guardian/mobile-n10n).

# Check formatting without writing
bun run format:check
```
## 2. Getting Started

Install the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) VSCode extensions to get inline linting and format-on-save.
### Prerequisites

## Install dependencies
- This project relies on [Bun](https://bun.com/). On Mac OS install its latest version using Homebrew:
```sh
brew install bun
```
- [dev-nginx](https://github.com/guardian/dev-nginx)
- Docker (optional; required for local Postgres if needed)

To setup [dev-nginx](https://github.com/guardian/dev-nginx) and install the dependencies for every app, package etc, simply invoke this while current working dir is the root of the repo:
### First-time setup

```sh
```bash
./scripts/setup.sh
```

## Install lint & format checks as a git hook using `lefthook`
### Run locally

The git hooks are installed automatically after `bun install` via the `prepare`
script. If you ever need to (re)install them manually, run:
Start both frontend and backend:

```sh
bunx lefthook install
```bash
./scripts/start.sh
```

Now, on each `git push`, ESLint and Prettier will check the files being pushed.

### To add dependencies
Local URLs:

As we rely on Bun on pretty much everything here, to install npm modules we need to use bun. An example:
- `https://dispatch.local.dev-gutools.co.uk`
- `https://dispatch-backend.local.dev-gutools.co.uk/`

```sh
bun add some_npm_dependency
Run apps separately if needed:

# dev dependency
bun add -D dev_dependency
```bash
cd src/apps/frontend
bun run dev

# targetting a specific app, package etc
bun --filter backend add some_npm_module
cd src/apps/backend
bun run dev
```

Bun will generate or update the existing `bun.lock` file, similar to `package-lock.json`.

## Docker compose

Should we require to rely on Postgres DB. There's a minimal working `./docker/docker-compose.local.yml` file and project root `package.json` contains two helper scripts to start & stop docker services.

To start services:

```sh
bun docker:compose:up
```
### Optional local Postgres

To stop them:
Should Postgres be required, there is a minimal working ./docker/docker-compose.local.yml file and two helper scripts to start & stop docker services.

```sh
bun docker:compose:down
```bash
bun run docker:compose:up
bun run docker:compose:down
```

## Local development
### Tests, linting, formatting, and type checks

To run [dev-nginx](https://github.com/guardian/dev-nginx) for the project and run the frontend and backend apps, run the start script from the root of the repo:
Run from the repo root:

```sh
./scripts/start.sh
```bash
bun test
bun run lint
bun run lint:fix
bun run format
bun run format:check
bun run typecheck
```

The app will be available on:
Run commands for one workspace package/app when needed:

- https://dispatch.local.dev-gutools.co.uk
- https://dispatch-backend.local.dev-gutools.co.uk/
```bash
bun --filter backend test
bun --filter frontend typecheck
```

You can also run each app separately as below.
Git hooks are managed with `lefthook` and installed automatically via `bun install` (`prepare` script).

### Start backend server app
## 3. How It Works

Backend server app currently uses Express.js as its REST server. To start the backend app:
### Core technologies

```sh
cd ./src/apps/backend
bun run dev
```
- Bun workspaces for package management and scripts.
- React (frontend) and Express + Zod validation (backend).
- AWS CDK (`@guardian/cdk`) for infrastructure definitions.

The server will restart seamlessly upon any file changed during it's code changes.
### Repository layout

### Start frontend app
- `src/apps/frontend`: UI for composing notifications.
- `src/apps/backend`: API and channel request generation.
- `src/packages`: shared packages.
- `cdk`: infrastructure stack and deployment definitions.

Frontend app uses React. To start the app:
### Infrastructure model

```sh
cd ./src/apps/frontend
bun run dev
```

## Running tests
This is deployed using AWS API Gateway + Lambda.

To run tests in the scope of the entire project:
```mermaid
flowchart LR
Editor[Editorial user] --> APIGW[API Gateway\ndispatch.gutools.co.uk]
APIGW --> Lambda[Lambda\nNode.js 24.x\nExpress app via serverless adapter]

```sh
bun test
Lambda --> Braze[Braze API\nemail channel]
Lambda -. planned .-> N10N[mobile-n10n notifications API\napp push channel]
```

To run on a specific app or package there are couple of ways:
## 4. Useful Links

Having current working dir open inside that app:
- Braze REST API: https://www.braze.com/docs/developer_guide/rest_api/sending_messages
- App notifications monorepo: https://github.com/guardian/mobile-n10n
- Existing Breaking News tool: https://fronts.gutools.co.uk/breaking-news
- Existing Breaking News tool code: https://github.com/guardian/facia-tool
- Bun documentation: https://bun.sh/

```sh
cd ./src/apps/backend
bun test
```

Using workspace filters, ie:
## 5. Terminology

```sh
bun --filter backend test
```
- **Segment**: A target audience group (`UK`, `US`, `AU`, `EU`, `ALL`).
- **Delivery mode**: The notification timing strategy (`immediate`, `scheduled`, `intelligent`).
- **Channel**: A delivery destination such as `email` or `app-notification`.
106 changes: 106 additions & 0 deletions docs/ADRs/infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ADR: Infrastructure

**Date:** 2026-07-22
**Status:** Accepted
**Deciders:** Notifications Mission — Q2 2026 (temporary 12-week team)

---

## Context

Dispatch is an internal tool used by editorial staff to define and send notifications to Guardian audiences (e.g. mobile push notifications, newsletter emails). It must:

- Authenticate and validate inbound requests.
- Make requests to downstream services (e.g. Braze, mobile notifications API).
- Expose a small, stable RESTful surface (`/v1/notifications`, `/v1/channels/*`, health checks).

It is not a public-facing, high-traffic API. We expect usage to be limited to a small group of permission-ed users sending a few notifications per day.

Two hosting options have been considered:

1. **AWS EC2** — micro instance running an Express server as a long-lived process.
2. **AWS API Gateway + Lambda** — a serverless model where API Gateway routes requests to a Lambda function that contains the Express app (via a handler adapter `@codegenie/serverless-express`).

---

## Decision Drivers

- **Maintenance overhead** — infrastructure that requires minimal patching, capacity planning, and ongoing maintenance is preferred.
- **Cost** — the service has low and bursty traffic; paying for idle compute is wasteful.
- **Guardian CDK support** — the Guardian's shared CDK library (`@guardian/cdk`) provides first-class `GuApiLambda` and `GuEc2App` patterns.
- **Cold-start tolerance** — requests arrive from internal users who already accept a small interaction latency; sub-100 ms cold starts are not required.
- **Security** — managed runtimes and automatic patching reduce the attack surface without manual effort.
- **Scalability** — while traffic is low today, the architecture should not become a bottleneck if usage grows.

---

## Options Considered

### Option 1: EC2

Run an Express server on a Guardian-standard EC2 instance behind a load balancer.

| # | Pros |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1 | **Persistent connections** — long-lived TCP connections to downstream services (Braze, mobile-n10n) avoid repeated TLS handshakes and can benefit from connection pooling. |
| 2 | **Predictable, low latency** — no cold-start penalty; every request is handled by a warm process. |
| 3 | **Familiar runtime model** — standard Node.js/Express behaviour; no handler-adapter shim required. Libraries that use global state, timers, or open file handles behave as expected. |
| 4 | **Long-running tasks** — if the broker ever needs to hold a connection open (e.g. streaming, long polling), there is no execution time limit. |
| 5 | **Easier local parity** — `bun run dev` locally mirrors production directly; no local Lambda emulation needed. |

| # | Cons |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Always-on cost** — the instance runs 24/7 even when there is no traffic. For a low-volume internal tool this is unnecessary. |
| 2 | **OS / AMI maintenance** — the team must keep the base AMI patched and rotated. Guardian tooling automates much of this, but it still requires periodic attention. |
| 3 | **Capacity planning** — instance type must be chosen upfront; under-sizing risks OOM/CPU issues; over-sizing wastes money. |
| 4 | **Slower deployments** — rolling AMI updates or instance replacements are slower than a Lambda code push. |
| 5 | **More IAM / networking surface** — an EC2 instance in a VPC requires security groups, subnets, and instance profiles alongside the application code. In practice this is mostly abstracted by GuCDK. |

---

### Option 2: API Gateway + Lambda (Serverless)

Host the Express app behind API Gateway, with the Lambda runtime wrapping the existing handler via a serverless adapter.

| # | Pros |
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Pay-per-request** — Lambda charges only for invocation duration; idle periods cost nothing. |
| 2 | **Zero server management** — no AMI patching, OS updates, or instance lifecycle to manage. AWS handles runtime security patches for Node.js 24.x automatically. |
| 3 | **Automatic scaling** — Lambda scales horizontally per request without any configuration. |
| 4 | **Fast deployments** — uploading a new zip file and updating the Lambda completes in seconds, making CI/CD fast. |
| 5 | **Native Guardian CDK support** — `GuApiLambda` provides most of what we need out of the box. |
| 6 | **Built-in structured logging & tracing** — Lambda integrates with CloudWatch Logs, and the Guardian's Kinesis log shipping without additional configuration. |
| 7 | **Concurrency rate-limiting** — `reservedConcurrentExecutions: 1` (already set in CDK for CODE) provides a simple hard cap that protects downstream services and our other Lambda processes. |

| # | Cons |
| --- | ---- |

| 1 | **Cold starts** — a Lambda that has been idle for several minutes will incur a cold start (~200–500 ms for a Node.js 24.x function at typical bundle sizes). [Provisioned Concurrency](https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html) can eliminate this if it becomes a problem. |

| 2 | **15-minute execution limit** — individual Lambda invocations cannot exceed 15 minutes. Not a concern for the application's current requirements, but rules out ever running long batch jobs inside the same function. |
| 3 | **Stateless by design** — in-memory caches are not shared across concurrent Lambda instances. Shared state requires an external store (ElastiCache, SSM Parameter Store, DynamoDB). |
| 4 | **Adapter shim required** — the Express app must be wrapped with a serverless adapter (`@codegenie/serverless-express`). This adds a thin layer of indirection that must be kept up to date. |
| 5 | **Local development environment** — running the app locally with `bun run dev` uses plain Express; the Lambda handler path is only exercised in deployed environments (or with a local emulator such as SAM CLI). |
| 6 | **Larger payloads** — API Gateway has a 10 MB request/response payload limit. Not a concern for the current notification payloads, but worth noting if large assets are ever handled. |

---

## Decision

**Use AWS API Gateway + Lambda** via the Guardian CDK `GuApiLambda` pattern.

The serverless model eliminates operational overhead, aligns with Guardian CDK conventions, and is cost-optimal for the workload. The identified cons (cold starts, 15-minute limit, stateless caches) either do not apply to this use case or can be mitigated inexpensively if they become issues in future.

EC2 would only become preferable if one or more of the following conditions held:

- Sustained high-throughput traffic making Lambda per-invocation pricing more expensive than a reserved instance.
- A hard requirement for persistent server-side connections or long-running jobs within the application itself.

---

## Consequences

- The CDK stack uses `GuApiLambda` (see `cdk/lib/notifications.ts`). This is the single authoritative infrastructure definition; no EC2 resources are provisioned.
- The Express app in `src/apps/backend` is wrapped by a Lambda handler, which adapts API Gateway proxy events to Express.
- Any shared state must be externalised to AWS SSM Parameter Store, or a similar service rather than held in process memory.
- If cold-start latency becomes noticeable to users, Provisioned Concurrency should be evaluated before considering a move to EC2.
Loading