A monorepo for Guardian notifications tooling.
It currently contains a single editorial tool called Dispatch (under development), used to compose and send notifications.
Dispatch gives editorial staff one place to compose and send breaking-news notifications.
It replaces a fragmented workflow spread across multiple systems and legacy tooling.
Email delivery currently integrates with Braze, and app push integration with the notifications API.
- This project relies on Bun. On Mac OS install its latest version using Homebrew:
brew install bun
- dev-nginx
- Docker (optional; required for local Postgres if needed)
Install dev-nginx before ./scripts/setup.sh can be run:
brew tap guardian/homebrew-devtools
brew install guardian/devtools/dev-nginxThen:
./scripts/setup.shDispatch protects its endpoints with two independent checks, and both must be
satisfied before authorised endpoints (for example GET /v1/user,
POST /v1/notifications, POST /v1/notification-tests) return anything other
than 401/403:
- Authentication — a valid pan-domain cookie (
gutoolsAuth-assym) for thelocal.dev-gutools.co.ukdomain. panda-auth for Node cannot mint this cookie itself, so you must run the login tool locally to sign in and issue it. - Authorisation — your user must hold the
dispatch_accesspermission. The permissions store is read from the CODE bucket using thecomposerAWS profile. Grant yourself the permission via the CODE permissions admin UI at permissions.code.dev-gutools.co.uk/admin: find your user and enabledispatch_access.
Both the login tool and Dispatch read from the same Composer AWS account, so
a single set of Janus credentials under the composer profile satisfies cookie
issuance and the permissions lookup at once.
Fetch fresh Janus credentials for the Composer
account into the composer profile (Janus -> Composer -> Run dispatch locally). Both the login tool and Dispatch expect
this profile to be present, so grab them before starting either service.
Clone and start the login tool in a separate checkout so it is available alongside Dispatch:
# in your local checkout of guardian/login
./script/startThis serves https://login.local.dev-gutools.co.uk, which Dispatch redirects to
when you are unauthenticated.
./scripts/start.shLocal URL:
https://dispatch.local.dev-gutools.co.uk
Open https://dispatch.local.dev-gutools.co.uk. When unauthenticated you are
redirected to login.local.dev-gutools.co.uk; sign in there to mint the
gutoolsAuth-assym cookie, then you are returned to Dispatch. With the cookie
present, authorised endpoints resolve successfully.
This local setup currently depends on a temporary workaround. Because panda-auth for Node does not generate cookies, you need to run the login tool locally alongside Dispatch. This is implemented using the new developer policies. We considered adding these policies to login directly, but both login and Dispatch use the same Composer AWS profile, so only one policy context can be active at a time.
Run apps separately if needed:
cd src/apps/frontend
bun run dev
cd src/apps/backend
bun run devSpin a new docker container running Postgres with
bun run db:startAnd stop it with
bun run db:stopRun from the repo root:
bun test
bun run lint
bun run lint:fix
bun run format
bun run format:check
bun run typecheckRun commands for one workspace package/app when needed:
bun --filter backend test
bun --filter frontend typecheckGit hooks are managed with lefthook and installed automatically via bun install (prepare script).
- Bun workspaces for package management and scripts.
- React (frontend) and Express + Zod validation (backend).
- AWS CDK (
@guardian/cdk) for infrastructure definitions.
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.
This is deployed on AWS with API Gateway, Lambda, using an RDS database.
flowchart LR
Editor[Editorial user] --> APIGW[API Gateway custom domain\ndispatch.gutools.co.uk]
APIGW --> Lambda
subgraph VPC[Account VPC]
subgraph SUBNET1[PUBLIC subnet]
subgraph SG1[Lambda security group]
Lambda[Lambda\nNode.js 24.x\nExpress app via serverless adapter]
end
end
subgraph SUBNET2[PRIVATE subnet]
subgraph SG2[Database security group]
RDS[(RDS PostgreSQL 18\ndispatch DB)]
end
end
end
Lambda --> Braze[Braze API\nemail channel]
Lambda -. planned .-> N10N[mobile-n10n notifications API\napp push channel]
Lambda -->|TCP 5432| RDS
- 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/
See CONTEXT.md for the project glossary — the canonical name for each domain concept, and the near-synonyms to avoid. It is the single source; do not restate definitions here.