Tamanu is an open-source patient-level electronic health records system for mobile and desktop.
Canopy provides:
- a server discovery service for the Tamanu mobile app
- the full list of available versions of Tamanu
- download URLs to available artifacts for active versions
and, internally:
- a global view of server status and healthchecks
- backup management
- associated tooling (bestool)
This is not expected to be usefully run by any other organisation; as a public-interest non-profit, BES International publishes almost all of its software as open-source.
We have a container image for linux/amd64 and linux/arm64:
ghcr.io/beyondessential/canopy:latest
Each push to main builds and publishes a new image (also tagged
sha-<short> for the source commit) and triggers a pulumi deploy.
- Install Rustup, which will install Rust and Cargo.
- Install just command runner
- Clone the repo via git:
$ git clone git@github.com:beyondessential/canopy.git- Install development dependencies:
$ just install-depsThis will install cargo-nextest, diesel CLI, and watchexec.
- Create a new blank postgres database.
- Optionally set the
DATABASE_URLenvironment variable (if your database isn't named the defaultcanopy):
$ export DATABASE_URL=postgres://localhost/canopy_dev- Run migrations:
$ just migrate- Build the project:
$ just check- Run public server:
$ cargo watch-public- Run private server:
$ just watch-private- Run other binaries:
$ cargo run --bin binary_name_here- Tests:
$ just test- Lints:
$ just lint- Format, lint, and test in one command:
$ just devSee all available commands:
$ just --listWe recommend using Rust Analyzer or Rust Rover for development.
- Create a migration:
$ just migration some_name_here-
Write the migration's
up.sqlanddown.sql -
Run the pending migrations:
$ just migrate- Test your down:
$ just migrate-redoYou'll need to have kubectl installed and authorised.
# just download-db {database name} {kubernetes namespace} [dump file]
$ just download-db canopy canopy-prodThe public-server binary serves the public API and views, which are expected to be exposed to
the internet (in production behind an ingress gateway or reverse proxy).
A client is identified by an X509 certificate presented in a header, PEM-encoded and optionally URL-encoded.
Which header carries it depends on what terminates TLS in front of the server, and is chosen by CANOPY_DEVICE_AUTH_CERT_HEADER:
mtls(ornginx) — themtls-certificateheader, falling back tossl-client-cert. The default, and the live ingress path.xfcc(orenvoy) — thex-forwarded-client-certheader, in Envoy's format.
Exactly one header is read. The other is ignored rather than tried as a fallback, so a client that can set a header it isn't meant to cannot present an enrolled device's certificate and be resolved as that device — the certificate is a public key, not a secret, and nothing at this layer proves possession. An unrecognised value for the setting keeps the default rather than guessing.
For the same reason, an x-forwarded-client-cert chain is read from its last element, which is the one the terminating proxy appended; an element a client put there first is not trusted.
To get a certificate, run:
$ just identityThis will write the identity.crt.pem and identity.key.pem.
You can then put it in an environment variable:
$ export MTLS_CERT="$(jq -sRr @uri identity.crt.pem)"and then use curl like:
$ curl -H "mtls-certificate: $MTLS_CERT" ...Against a server configured for XFCC, the same certificate goes in the Envoy header instead:
$ curl -H "x-forwarded-client-cert: Cert=$MTLS_CERT" ...In production, the header should be set from a client certificate, as terminated by a reverse proxy or load balancer, and any matching header on the incoming requests should be stripped.
- Nginx: use the
$ssl_client_escaped_certvariable. - Caddy: use the
{http.request.tls.client.certificate_pem}placeholder. - Envoy: enable
forward_client_cert_detailswithset_current_client_cert_details.cert, and setCANOPY_DEVICE_AUTH_CERT_HEADER=xfccso that header is the one trusted.
Claude Code:
$ claude mcp add --transport http canopy https://canopy.tail53aef.ts.net/api/mcpThen ask it things like "list the servers in group X" or "which backups are overdue".