Configuration Sources & Priority
SeqDesk resolves configuration from four sources. This page states the order precisely, explains the two-stage merge that produces it, and — most usefully — lists which subsystem actually reads the resolved value, because several do not.
Priority order
From highest to lowest priority:
| Priority | Source | Label in the API | Best for |
|---|---|---|---|
| 1 (highest) | Environment variables (SEQDESK_*) | env | Secrets, per-deployment overrides, CI |
| 2 | Config file (settings.json) | file | Reproducible non-secret deployment description |
| 3 | Database (SiteSettings, edited in the Admin UI) | database | Day-to-day operational changes |
| 4 (lowest) | Built-in defaults | default | Sensible starting values |
How the merge actually runs
Resolution happens in two stages, and knowing that explains most of the odd behaviour operators run into.
Stage 1 — the file/env layer. The loader walks the working directory for the
first config file it finds, parses it, reads the SEQDESK_* variables it knows
about, and deep-merges:
defaults ← config file ← environment variablesDeep-merge means objects are combined key by key rather than replaced wholesale.
Setting pipelines.execution.slurm.queue in the file leaves the other
slurm.* defaults intact. Arrays are replaced, not merged: a file-level
sequencingFiles.extensions overrides the whole default list.
Stage 2 — the database layer. The SiteSettings row is read and slotted
underneath the result of stage 1. A database value is used only where stage 1
produced nothing but a default. Where stage 1 produced env or file, the
database value is discarded and the source label stays env or file.
The final object plus its source map is cached in-process for 60 seconds. Saving through the Admin UI clears that cache for the process that handled the request; other worker processes pick the change up when their cache expires.
The source map only covers keys that have a default
Source labels are produced by walking the built-in default configuration and asking, for each leaf it finds, whether the file or the environment supplied a value. A key that does not exist in the defaults therefore never gets a source label at all.
This matters because pipeline execution deliberately ignores any value whose
source is not file or env — that is how it keeps the Admin UI in charge of
settings you have not pinned in a file. Combine the two rules and one key falls
through the gap:
pipelines.execution.slurm.options has no built-in default, so it
never receives a source label, so the pipeline executor drops it. Extra
sbatch flags set in settings.json are silently
ignored. There is also no SEQDESK_SLURM_OPTIONS variable. Set
extra SLURM options in Settings → Infrastructure → Pipeline
Runtime, or at install time with
SEQDESK_EXEC_SLURM_OPTIONS, both of which write
SiteSettings.extraSettings.pipelineExecution.slurmOptions.
Every other key documented in the
Config File Reference that the executor reads
(mode, runDirectory, slurm.enabled, slurm.queue, slurm.cores,
slurm.memory, slurm.timeLimit, conda.path, conda.environment,
conda.cacheDir, pipelines.databaseDirectory) does have a default and does
work from a file or the environment.
Who reads the resolved configuration
This is the table to check first when a setting does not appear to do anything.
“Resolved config” means the merged object described above; “SiteSettings” means
the feature reads the database row directly and never consults the merge.
| Area | Read from | Notes |
|---|---|---|
| Pipeline execution (mode, SLURM, Conda, run/database directories) | Resolved config over SiteSettings.extraSettings.pipelineExecution | File/env values win; anything else falls back to the admin-saved values |
| Which pipelines are offered | PipelineConfig rows, then SiteSettings.extraSettings.installProfilePipelineAllowlist | pipelines.enabled in the config file is an install-time input, not this |
site.dataBasePath | Resolved config if its source is env/file, else SiteSettings.dataBasePath | See Data Storage |
| Notifications (relay + in-app) | Resolved config | Also requires the Email Notifications module to be enabled |
| Telemetry | SiteSettings.extraSettings.telemetry over resolved config | Inverted precedence — see below |
| ENA credentials and test mode | SiteSettings.enaUsername / enaPassword / enaTestMode | Passwords are encrypted at rest |
| Sequencing file discovery (extensions, scan depth, ignore patterns, auto-assign) | SiteSettings.extraSettings.sequencingFiles | |
Registration policy (auth.allowRegistration) | SiteSettings.extraSettings.auth | |
Access policy (access.*) | SiteSettings.extraSettings and SiteSettings.postSubmissionInstructions | |
Optional module settings (moduleSettings.*) | SiteSettings.extraSettings.accountValidationSettings / billingSettings | |
Runtime secrets (DATABASE_URL, NEXTAUTH_SECRET, …) | process.env, seeded once at start from settings.json runtime | Restart required |
For every row whose source is SiteSettings, the matching settings.json
section is still meaningful — but as an install-time input. The installer,
--reconfigure, a hosted install profile, and
Settings → Infrastructure → Import settings.json all read those sections and
write them into the database. After that, the database is authoritative.
The telemetry exception
Telemetry deliberately inverts the order: a value stored in
SiteSettings.extraSettings.telemetry wins over telemetry.* from
settings.json or SEQDESK_TELEMETRY_ENABLED. The intent is that a facility
admin can always turn the heartbeat off from the UI without editing files.
Above all of that sits a kill switch: SEQDESK_TELEMETRY_DISABLED=true forces
telemetry off before any source is consulted. See
Operational Telemetry.
When to use each source
Environment variables
Use them for secrets (DATABASE_URL, NEXTAUTH_SECRET,
SEQDESK_NOTIFICATION_RELAY_TOKEN), for values that differ between staging and
production, and for anything your orchestration already injects. They win over
everything else, which makes them the right tool for “this box is different” and
the wrong tool for “this is how the facility works”.
Full list: Environment Variables.
The config file
Use settings.json for the reproducible description of a deployment: port, site
identity, data paths, pipeline execution policy, and the install-time inputs that
seed the database. It is the artefact you can put under version control (with
secrets stripped) and hand to a colleague to rebuild the same facility.
Discovery order, in the working directory SeqDesk starts in:
settings.json— canonicalseqdesk.config.json— legacy/development fallback.seqdeskrc.seqdeskrc.json
The first file found wins; the others are not read. Full key reference: Config File Reference.
The database (Admin UI)
Use the Admin UI for day-to-day operational change: ENA credentials, file discovery rules, access policy, module toggles, pipeline runtime tuning. It is the only source a non-technical admin can edit, and for several features it is the only source that is read at all.
The trade-off is that a value pinned in settings.json or the environment makes
the corresponding UI field cosmetic — it saves, but the pinned value keeps
winning. See Runtime Settings.
Inspecting the active configuration
Facility admins can read the resolved configuration and its source map on the
Platform Info page (sidebar Settings → Info, /admin/settings) under
Configuration Sources, or directly:
GET /api/admin/config/statusThe endpoint requires a FACILITY_ADMIN session and returns 401 otherwise.
The response has four fields:
| Field | Meaning |
|---|---|
config | The resolved configuration, with secrets replaced by ******** |
sources | Map of dotted config path to env, file, database, or default |
filePath | Absolute path of the config file that was loaded, or absent if none was found |
loadedAt | When this resolution was performed |
Masked fields are ena.password, notifications.relayToken, and the
runtime secrets nextAuthSecret, anthropicApiKey, adminSecret, and
blobReadWriteToken. A masked value tells you the secret is set, not what it
is.
Worked example
A facility runs pipelines on SLURM, keeps the Webin password out of files, and lets admins tune the queue from the UI.
/opt/seqdesk/settings.json (mode 600):
{
"site": {
"name": "HZI Sequencing Core",
"dataBasePath": "/mnt/sequencing/data"
},
"pipelines": {
"enabled": true,
"execution": {
"mode": "slurm",
"runDirectory": "/mnt/sequencing/pipeline_runs",
"conda": {
"path": "/net/software/conda",
"environment": "seqdesk-pipelines",
"cacheDir": "/net/software/conda-cache"
}
}
}
}The systemd unit adds only secrets:
DATABASE_URL=postgresql://seqdesk:...@db.internal:5432/seqdesk?schema=public
NEXTAUTH_SECRET=...
SEQDESK_NOTIFICATION_RELAY_TOKEN=...The resulting source map:
| Path | Value | Source | Editable in the Admin UI? |
|---|---|---|---|
site.name | HZI Sequencing Core | file | No — the file wins |
site.dataBasePath | /mnt/sequencing/data | file | No — the file wins |
pipelines.execution.mode | slurm | file | No — the file wins |
pipelines.execution.slurm.queue | cpu | default | Yes — nothing pins it, so the admin-saved value applies |
pipelines.execution.slurm.cores | 4 | default | Yes |
ena.testMode | true | default | Yes — and the UI value is what submissions use |
The deliberate choice here is to pin the things that must not drift (paths, execution target) and leave queue sizing to the people watching the cluster.
When a setting appears to be ignored
Work through these in order.
1. Check the source label. GET /api/admin/config/status and look up the
dotted path in sources. If it says env or file, the Admin UI is not the
thing to change. If it says default when you expected file, the file is not
being read — check filePath in the same response.
2. Check that the file is where SeqDesk looks. The loader searches the
process working directory. In a release-layout install that is
<root>/current, which holds a symlink to <root>/settings.json. A file you
created at <root>/current/settings.json by hand replaces that symlink and will
be lost at the next update.
3. Check the file parses. Invalid JSON is not fatal: the loader logs a
warning to the server log and continues with environment variables, the
database, and defaults. filePath will still be reported, so a config file that
“is found but does nothing” is almost always a JSON syntax error. Validate with
node -e "JSON.parse(require('fs').readFileSync('settings.json','utf8'))".
4. Check the 60-second cache. A database change made in one process is not visible in another until its cache expires. Wait a minute before concluding anything.
5. Check whether the feature reads the resolved config at all. Consult the
consumer table above. ena.*, access.*, auth.*, sequencingFiles.* and
moduleSettings.* in settings.json are install-time inputs; on a running
server the corresponding feature reads the database. To push them into the
database on an existing install, use
Settings → Infrastructure → Import settings.json or re-run the installer
with --reconfigure.
6. Check for a restart-only value. runtime.databaseUrl, runtime.directUrl,
runtime.nextAuthUrl and runtime.nextAuthSecret are copied into process.env
once at process start, and only if the corresponding variable is not already
set. Editing them requires a restart, and an environment variable of the same
name always wins.
Best practices
- Keep secrets out of committed files.
settings.jsonis written mode600by the installer; keep it that way. - Version-control a sanitised template, and generate the populated file from your deployment automation or a hosted install profile.
- Pin only what must not drift. Every value you pin in a file removes a knob from the Admin UI.
- Write down why you pinned something. JSON has no comments; keep the rationale in the automation that generates the file.