An app to manage patient records, consultations, appointments, lab reports, ...
The stack (provided by Meteor):
The tests are declared via Mocha thanks to
meteor-testing:mocha.
π‘ We would replace Mocha by AVA or Jest here and now if only there was a Meteor package for that.
User Interface tests are facilitated by
@testing-library/react,
@testing-library/dom,
and
@testing-library/user-event.
β οΈ Code coverage currently includes some of the test files. Waiting for a solution where one can list coverage for source and test files separately.
In what follows, dev refers to the development machine, and prod refers to
the production machine.
curl https://install.meteor.com | sh
git clone gh:infoderm/patients
cd patients
meteor npm ci
This will run the linter and the type checker on staged files before each commit.
meteor npm run install-hooks
To lint source files we use xo with
configuration inside package.json. You can run the linter with
meteor npm run lint
You can attempt to autofix some errors with
meteor npm run lint-and-fix
The entire code base is checked for type errors with tsc, the
TypeScript compiler. You can run the type
checker with
meteor npm run tsc
To run it in watch mode during development use
meteor npm run tsc:watch
You can set host and port via the
$HOSTand$PORTenvironment variables (defaultlocalhost:12348).
meteor npm run test
To run client tests non-interactively you can use
NB: this uses
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
meteor npm run test:dev:non-interactive
You can set host and port via the
$HOSTand$PORTenvironment variables (defaultlocalhost:12348).
meteor npm run test -- --once
β οΈ We recommend using thechromiumexecutable of your distribution. Installation of the puppeteerchromiumexecutable can be avoided by placing the linepuppeteer_skip_chromium_download=truein your~/.npmrc. If you wish to use thechromiumexecutable that comes withpuppeteerremove the assignment of the variablePUPPETEER_EXECUTABLE_PATH.
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium meteor npm run ci:test
You can set host and port via the
$HOSTand$PORTenvironment variables (defaultlocalhost:12345).
meteor npm run dev
You can set host and port via the
$HOSTand$PORTenvironment variables (defaultlocalhost:12345).
meteor npm run bundle-visualizer
meteor npm run upgrade
Some dependencies need manual upgrade. Their versions depends on the used Meteor version. Hereunder are the information links for the latest stable release of Meteor:
NB: @types/mongodb does not need explicit pinning because it is a dependency
of @types/meteor.
Direct ESM dependencies cannot be added and CJS dependencies cannot be upgraded to ESM. See the relevant discussion.
pacman -S docker
systemctl enable --now docker
useradd -m patient
gpasswd -a patient wheel
gpasswd -a patient docker
su patient && cd
git clone https://github.com/infoderm/patients && cd patients
To deploy a published image. Each git version tag is published automatically (v*).
Recent commits and PRs also have published images available (edge, sha-*, pr-*).
The complete list of currently available tagged images can be consulted at:
Current deployment runs an unmodified mongo:${MONGO_VERSION} image. The
default MONGO_VERSION is the recommended version with the currently checked
out sources.
The compose.yaml file defines:
- a volume to persist the database,
- a healthcheck that configures the replica set and ensures it is healthy,
- a strict network configuration that only allows
patient-webandpatient-backupto connect, - logging to JSON files
The user-facing root URL of the deployed app.
For instance, without this, dynamic imports will not work.
The port at which the web client is exposed. Proxy this port if you want to be able to reach the app from another machine.
How many proxies lie between the user and the prod machine. Essential to
correctly configure IP-address-based rate-limiting.
To use sane defaults METEOR_SETTINGS="$(jq -c < .deploy/default/settings.json)".
A custom settings.json
file can also be created and used instead.
The public age key used to encrypt the backup. This is the public key output
to stderr at key generation time (age-keygen). It is also present as a
comment in the generated private key file (the private key is only useful to
restore backups and is not needed by the backup container).
Where to store the backups.
crond schedule for backups.
Expected interval in seconds between backups (add some buffer). Derive this from backup schedule.
The backup container will be considered unhealthy if no backup happens in this interval.
crond schedule for the backup retention policy.
Expected interval in seconds between runs of the backup retention policy (add some buffer). Derive this from backup schedule.
The backup retention policy container will be considered unhealthy if no backup happens in this interval.
We recommend you name your deployment uniquely, for instance using its domain name:
DEPLOYMENT=.deploy/example.local
We recommend you create a directory where the deployment files will be stored:
mkdir "${DEPLOYMENT}"We recommend you create a copy of the default METEOR_SETTINGS so that you can
tweak them if needed:
cp .deploy/default/settings.json "${DEPLOYMENT}/settings.json"Here is an example deployment compose configuration without backups
(IMAGE_TAG>=v2025.01.29-1):
IMAGE_TAG=v2025.01.29-1 \
ROOT_URL=https://example.local PORT=3000 HTTP_FORWARDED_COUNT=2 \
METEOR_SETTINGS="$(jq -c < "${DEPLOYMENT}/settings.json")" \
docker compose \
-f compose.yaml \
-f .deploy/ghcr.io/patient-web.yaml \
config > "${DEPLOYMENT}/compose.yaml"We recommend you save this script at
${DEPLOYMENT}/compose.shfor when you wish to update your configuration.
Example with encrypted backups (IMAGE_TAG>=v2025.02.15-1,
requires a key pair generated by age-keygen):
IMAGE_TAG=v2025.02.15-1 \
ROOT_URL=https://example.local PORT=3000 HTTP_FORWARDED_COUNT=2 \
METEOR_SETTINGS="$(jq -c < "${DEPLOYMENT}/settings.json")" \
+ BACKUP_KEY="<AGE-PUBLIC-KEY>" \
+ BACKUP_DIR="./${DEPLOYMENT}/backups" \
+ BACKUP_SCHEDULE="0 21 * * *" \
+ BACKUP_INTERVAL="129600" \
+ BACKUP_RETENTION_POLICY_SCHEDULE="0 18 * * 0" \
+ BACKUP_RETENTION_POLICY_INTERVAL="691200" \
docker compose \
-f compose.yaml \
+ -f .deploy/backup/compose.yaml \
+ -f .deploy/backup-retention-policy/compose.yaml \
-f .deploy/ghcr.io/patient-web.yaml \
+ -f .deploy/ghcr.io/patient-backup.yaml \
+ -f .deploy/ghcr.io/patient-backup-retention-policy.yaml \
config > "${DEPLOYMENT}/compose.yaml"Just remove IMAGE_TAG=... and the lines that configure pulling from
ghcr.io:
- IMAGE_TAG=v2025.02.15-1 \
ROOT_URL=https://example.local PORT=3000 HTTP_FORWARDED_COUNT=2 \
METEOR_SETTINGS="$(jq -c < "${DEPLOYMENT}/settings.json")" \
BACKUP_KEY="<AGE-PUBLIC-KEY>" \
BACKUP_DIR="./${DEPLOYMENT}/backups" \
BACKUP_SCHEDULE="0 21 * * *" \
BACKUP_INTERVAL="129600" \
BACKUP_RETENTION_POLICY_SCHEDULE="0 18 * * 0" \
BACKUP_RETENTION_POLICY_INTERVAL="691200" \
docker compose \
-f compose.yaml \
-f .deploy/backup/compose.yaml \
-f .deploy/backup-retention-policy/compose.yaml \
- -f .deploy/ghcr.io/patient-web.yaml \
- -f .deploy/ghcr.io/patient-backup.yaml \
- -f .deploy/ghcr.io/patient-backup-retention-policy.yaml \
config > "${DEPLOYMENT}/compose.yaml"Update ${DEPLOYMENT}/compose.yaml, then:
docker compose -f "${DEPLOYMENT}/compose.yaml" up -d
We recommend creating a deployment branch to keep track of the changes under
${DEPLOYMENT}, to maintain an history of upgrades (don't forget to create a.gitignoreto exclude the database backup files from the git history).
The current backup system dumps the database as an encrypted (age) compressed
MongoDB archive (--archive --gzip).
Backups can be automated, or they can be run manually:
docker exec -i patient-patient-db-1 \
mongodump --uri "${MONGO_URL}" --archive --gzip |
age -r "${AGE_PUBLIC_KEY}" > "backups/$(date '+%Y-%m-%d_%H:%M:%S')"
They can be restored with
age --decrypt -i "${AGE_PRIVATE_KEY}" < backups/YYYY-MM-DD_HH:mm:ss |
docker exec -i patient-patient-db-1 \
mongorestore --uri "mongodb://localhost:27017" \
--drop \
--nsInclude 'meteor.*' \
--nsFrom 'meteor.*' \
--nsTo 'meteor.*' \
--archive --gzip