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 is currently both incomplete and broken most probably due to a TypeScript/Istanbul incompatibility.
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
$HOST
and$PORT
environment 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
$HOST
and$PORT
environment variables (defaultlocalhost:12348
).
meteor npm run test -- --once
β οΈ We recommend using thechromium
executable of your distribution. Installation of the puppeteerchromium
executable can be avoided by placing the linepuppeteer_skip_chromium_download=true
in your~/.npmrc
. If you wish to use thechromium
executable that comes withpuppeteer
remove 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
$HOST
and$PORT
environment variables (defaultlocalhost:12345
).
meteor npm run dev
You can set host and port via the
$HOST
and$PORT
environment 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 meteorapp
gpasswd -a meteorapp wheel
gpasswd -a meteorapp docker
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-web
andpatient-backup
to 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.
git clone https://github.com/infoderm/patients && cd patients
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.sh
for 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.gitignore
to exclude the database backup files from the git history).
This method is last known to have worked with v2023.09.21-1
.
ssh-keygen -m PEM -t rsa -b 4096 -a 100 -f .ssh/meteorapp
Append it to /home/meteorapp/.ssh/authorized_keys
.
Remember: chmod .ssh 700
and chmod .ssh/authorized_keys 640
.
Install dependencies, custom certificates, and MongoDB on server:
meteor npm run setup-deploy
meteor npm run build-and-upload
meteor npm run deploy
TAG=vYYYY.MM.DD meteor npm run deploy
The current backup system requires age
and the encryption/decryption key at
~/key/patients
on the production machine. It saves the database as an
encrypted (age
) compressed MongoDB archive (--archive --gzip
).
sh .backup/fs/backup.sh
sh .backup/restore.sh
The backup system uses encrypted (age
) compressed MongoDB archives
(--archive --gzip
). They can be restored with
age --decrypt -i "$KEYFILE" < 'patients.gz.age' |
mongorestore --drop --nsInclude 'patients.*' --archive --gzip
The backup system uses encrypted gzipped TAR archives. They can be processed by
first decrypting with age
to obtain a .gz
file, then decompressing and
unarchiving with tar xzf
to obtain a dump
directory, and finally using
mongorestore --drop --db patients
to restore the database.