---
title: "Environment variables"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Environment variables}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE)
```
Every `EPISODIC_*` variable is optional; each entry point works fine with the equivalent argument passed explicitly instead. Environment variables exist so an operator can configure a running instance (a `systemd` unit, a Docker container) without editing R code - set them once where the instance runs, and every entry point that falls back to them picks the setting up automatically.
| Variable | Used by | Meaning |
| --------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EPISODIC_DB` | `episodic_run_app()`, `episodic_add_user()` (`db_path` argument) | Path to the instance's SQLite database, or a `mysql://` DSN pointing at a MariaDB/MySQL database instead - see "Database backend" above. |
| `EPISODIC_LANGUAGE` | every `lang` argument - `episodic_run_app()`, `episodic_demo()`, and every renderer beneath them | Dashboard/report language: `en`, `ar`, `nl`, `fr`, `de`, `hi`, `zh`, or `es`, or one of the regional variants `en-US` and `es-419` (Latin American Spanish). `en` is British English and `es` is Spain's Spanish, so `en-GB` and `es-ES` name those exactly and are accepted as such. Case and separator do not matter (`en_us` is `en-US`). Defaults to `en` if unset. A region EpiSODIC does not ship falls back to the language itself, with a warning saying so once (`nl-BE` renders in Dutch); anything else falls back to `en` with a warning, once, not on every render. Also sets the document's `lang` and `dir`, so Arabic renders right to left. Fixed for the whole running app - there is no in-app language switcher. |
| `EPISODIC_CONFIG` | `episodic_run_cron()` (`episodic_config_path` argument), `episodic_run_app()` | Path to an instance override of configuration (pathogen thresholds, `same_place`/`rare_trigger`/Farrington settings, notification channels, and access:
require_login), overlaid key-by-key on the shipped `r EpiSODIC:::doc_system_file("inst/config/episodic_default_config.yaml")`. Set to a path that does not exist, or to a file that is not valid YAML, this stops with an error rather than falling back to the defaults; every key it sets is checked against the shipped defaults, so a misspelled key or a value of the wrong type is named rather than silently ignored. See [**Tuning detection for your catchment**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/tuning-detection.html) for the complete reference table of tunable keys, and [**Deployment**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/deployment.html)'s "Opening the app to anonymous visitors" for access:
require_login. |
| `EPISODIC_PATHOGEN_CONFIG` | `episodic_run_cron()` (`pathogen_config_path` argument) | Path to an instance override of per-pathogen parameters (episode length, case-free days, severity weight, MEM mode, serial interval, and the rest), overlaid row-by-row on the shipped `r EpiSODIC:::doc_system_file("inst/config/episodic_default_pathogen_config.csv")`. Write a CSV with a `pathogen` column and only the columns you want to change; non-empty values override the shipped default for that pathogen and column, empty cells keep it, and pathogens not in your file keep their shipped row entirely. New pathogens are added. Set to a path that does not exist, this stops with an error. See [**Tuning detection for your catchment**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/tuning-detection.html) for the column reference. |
| `EPISODIC_STYLE` | `episodic_palette()` (`palette_config_path` argument) | Path to an instance override of the UI colour palette (including the five `severity_*` roles that colour verdicts and MEM intensity bands alike) and typography (`font`, `font_size_base`), overlaid key-by-key on the shipped `r EpiSODIC:::doc_system_file("inst/config/episodic_default_style.yaml")`. The same palette styles the outbreak reports and the emails EpiSODIC sends. Each value is checked: a colour must be a quoted hex colour, `font` a CSS font-family stack and `font_size_base` a CSS length. A value that fails keeps the shipped value for that role only, and a path that does not exist or a file that is not valid YAML keeps the whole shipped palette - neither stops the dashboard, a report or an email, and both are shown on the Info screen, written to every run's log and raised once as a warning. Deliberately separate from `EPISODIC_CONFIG`: colour and typography are display concerns, never part of the detection-reproducibility guarantee. |
| `EPISODIC_GEO_DATA` | `episodic_geo_source_resolve()` (`path` argument) | Path to an `.rds` file holding your own geographic reference data (an `sf` object with `pc`/`geometry` columns) for the choropleth map - any country, any area unit. There is no default: with this unset the dashboard shows its bar-chart fallback instead of a map, since a default map is a default *country*. EpiSODIC does bundle one worked example (Dutch postcodes) that `episodic_demo()` points this at. See [**Getting your data in**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/data-format.html)'s "Geographic reference data" section. |
| `EPISODIC_GEO_DATA_OVERLAY` | `episodic_geo_overlay_resolve()` (`path` argument) | Path to an `.rds` file holding an optional region-outline overlay (an `sf` object with just a `geometry` column), drawn on top of the choropleth. No default. See [**Getting your data in**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/data-format.html)'s "Geographic reference data" section. |
| `EPISODIC_PC_PROVINCE_MAP` | `episodic_pc_to_province()` (`path` argument) | Path to a CSV (`pc`, `province_code` columns) mapping your postcodes to provinces/states/regions, for L4 stream detection and for naming the province beside each postcode in the dashboard. Unset, no postcode resolves to a province and L4 simply has nothing to detect on - there is no built-in rule, because deriving a province from a postcode is country-specific; set but unusable (no such file, unreadable, no rows, wrong columns, or a repeated `pc`) stops the run and says which of those it is, rather than falling back. See [**Getting your data in**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/data-format.html)'s "Geographic reference data" section. |
| `EPISODIC_QUARTO_REPORT` | `episodic_report_render()` (`qmd_path` argument) | Path to an operator's own Quarto report template, overriding the shipped `r EpiSODIC:::doc_system_file("inst/report/episodic_default_report.qmd")`. Set to a path that does not exist, this stops with an error rather than silently sending reports out under EpiSODIC's own layout instead of yours. See "Custom report templates" above. |
| `EPISODIC_MAIL_TEMPLATE_NEW_CLUSTERS` | `episodic_run_cron()`, `episodic_mail_template_preview()` | Path to an operator's own Quarto (`.qmd`) template for the body of the email sent when a run detects new clusters. Unset, the built-in body is used. Only the email channels (`smtp`, `sendmail`, `microsoft365`) are affected; ntfy, Teams and Slack keep their own messages. A template that cannot be used (no such file, not a `.qmd`, Quarto not installed, or a render that fails) never costs an alert: the email goes out with the built-in body, and the run log, the Info screen and `episodic_notify_test()` say why. See [**Notifications**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/notifications.html)'s "Your own email layout". |
| `EPISODIC_MAIL_TEMPLATE_REPORT` | `episodic_run_cron()`, `episodic_mail_template_preview()` | The same, for the body of the email a scheduled outbreak report is attached to. See [**Scheduled reports**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/scheduled-reports.html). |
None of these need to be set to run the demo - `episodic_demo()` uses a temporary SQLite file and the shipped defaults, and writes its own geography and an illustrative pathogen overlay beside it. See [**Deployment**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/deployment.html) for `EPISODIC_DB` and `EPISODIC_CONFIG`/`EPISODIC_STYLE` in context (database backend, custom report templates), and [**Getting your data in**](https://certe-medical-epidemiology.github.io/EpiSODIC/articles/data-format.html)'s "Geographic reference data" section for the detail behind `EPISODIC_GEO_DATA` and `EPISODIC_GEO_DATA_OVERLAY`.
## Checking what they actually delivered
Setting a variable and seeing nothing change on screen has, until now, been indistinguishable from setting it wrongly. The dashboard's **Info** screen carries a "Reference data" panel that resolves each of these live and reports, per variable, whether the instance's own file is in use, the shipped default is standing in, nothing is configured, the file was configured and could not be used (with the reason), or an optional package the feature needs is missing - alongside what it put into the app: how many areas the map geometry holds, how many postcodes the province mapping maps and to how many provinces, and how many of the distinct postcodes in your own case data resolve to a province.
That last figure is the one worth reading first when a province is not appearing where you expect: a mapping read successfully but matching none of your postcodes looks, on every other screen, exactly like a mapping that was never read.
The resolved file paths are shown only to a signed-in user. By default the Info screen itself needs no sign-in, so the statuses and counts - the part that answers "is my file being used" - are readable without one, while the paths, which say more about the machine than a visitor needs, are not. On an instance with `access.require_login` set the whole screen is behind the sign-in like every other, so a signed-in reader sees the paths there too.