-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration
English · Tiếng Việt · 中文
Since 1.9.0, SkimMail configuration is split across three "planes" — three different places a setting can live, each with a different lifetime and a different way to change it. Understanding which plane a setting is on tells you exactly where to change it and when that change takes effect. The single rule that matters most: whatever you put in the environment always wins.
Five keys, and only five: DATA_DIR, DB_DRIVER, DATABASE_URL,
KEY_PROVIDER, LISTEN_ADDR. These exist to answer one question — where is
the database, and how do I open it? — which has to be answered before there
is a database to store anything else in.
-
DATA_DIRis environment-only, permanently. It cannot live anywhere else, because it names the directory everything else (including the file below) lives in. - The other four can be set either as environment variables, or written to
<DATA_DIR>/bootstrap.env— a small file SkimMail itself manages, via the first-run setup wizard's database step or viaskimmail config set. This file exists specifically so that someone who installed with a one-linecurl | shand wants Postgres has somewhere to put the DSN without editing a systemdEnvironmentFileby hand. - Precedence: environment beats the file, the file beats the built-in default.
- Changes here take effect on the next restart — nothing here is live-reloaded, because it decides which database process to open in the first place.
Worked example. A fresh apt install with no DATABASE_URL set in
/etc/default/skimmail runs on SQLite at /var/lib/skimmail/skimmail.db
(the built-in default). Pointing the first-run wizard at a Postgres instance
writes DB_DRIVER=postgres and DATABASE_URL=postgres://… into
/var/lib/skimmail/bootstrap.env and restarts the process onto it. If you
later add DATABASE_URL=… to /etc/default/skimmail itself, the environment
now wins — the value in bootstrap.env is ignored from that point on, even
though it's still sitting in the file.
The settings that decide how this SkimMail presents itself on the network:
public URL, base path, whether to trust a reverse proxy, extra CORS/CSRF
origins, the sign-in mode, and the Google/Microsoft OAuth app credentials.
Before 1.9.0 every one of these was an environment variable you could only
change by editing a file and restarting; now they are stored in the
database and editable from Settings in the browser, or from the shell
with skimmail config.
| Setting |
skimmail config key |
Environment variable |
|---|---|---|
| Public URL | base_url |
BASE_URL |
| Base path (subpath serving) | base_path |
BASE_PATH |
| Trust a reverse proxy | trust_proxy |
TRUST_PROXY |
| Extra CORS/CSRF origins | trusted_origins |
TRUSTED_ORIGINS |
| Sign-in mode | auth.mode |
AUTH_MODE |
| Google OAuth client ID / secret |
oauth.google.client_id / oauth.google.client_secret
|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
|
| Microsoft OAuth client ID / secret |
oauth.microsoft.client_id / oauth.microsoft.client_secret
|
MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET
|
- Precedence: environment always wins, and it's absolute. A field the
environment provides is shown locked in the browser, naming the exact
variable to remove if you'd rather manage it from the app instead. A write
attempt against a locked field — from the browser or from
skimmail config set— is refused, not silently overridden. - Changes take effect immediately — no restart. The server holds the resolved configuration behind a pointer it swaps on every save, which is what makes editing the OAuth app or the public URL from Settings work without a restart.
- OAuth client secrets are encrypted at rest with the master key (see
Security) and are never returned by any read — the API and
skimmail config listcan only report whether one is set. - The sign-in mode has one extra rule: once a credential exists, the browser
can no longer change it at all — only
skimmail config set auth.mode …on the host can, and setting it tononeadditionally requiresSKIMMAIL_ALLOW_AUTH_NONE=1in the environment. A stolen admin session cannot turn authentication off by itself.
Worked example. You set BASE_URL=https://mail.example.com in
/etc/default/skimmail because you manage this instance declaratively.
Settings ▸ (setup/about) now shows Public URL as locked, naming
BASE_URL as the variable to remove if you want to manage it from the
browser instead. Until you remove it there and restart, every edit attempt —
browser or skimmail config set base_url … — is refused with the same
message.
Runtime-tunable knobs for specific features: the idle screen-lock timeout and session lifetime (Security), sync concurrency/rate/depth and the IDLE/poll behaviour (Sync), and the log level and file rotation (Logs). These predate the 1.9.0 config work and follow a simpler, one-directional rule:
- The environment variable supplies the default the very first time — before anything has ever been saved for that setting.
- The moment you save a change from its Settings tab, the saved value wins from then on — including on every future restart — regardless of whether the environment variable is still set. There is no "locked" indicator here, because there is nothing to lock: once saved, the environment variable is simply not consulted again for that key.
- These are not covered by
skimmail config— that command is the escape hatch for Plane 0 and Plane 1 only.
Worked example. AUTO_LOCK_MINUTES is unset, so the screen lock starts
disabled (the built-in default is 0, never). You turn it on and set it to
15 minutes in Settings ▸ Security ▸ Timeouts. That 15 is now in the
database and applies on every restart. Setting AUTO_LOCK_MINUTES=30 in the
environment afterwards changes nothing — the value you saved already won,
and stays won until you change it again from the same Settings tab.
A few keys look like they belong to a feature above but are boot-only, full
stop — no Plane-1 lock, no Plane-2 override, ever: TRUSTED_PROXIES (see
Security), and LOG_FILE/LOG_FORMAT (the log path is
deliberately never settable from the API — that would be an arbitrary file
write from the browser).
"Plane" says where a saved change (if any) is kept; "env is…" says what the environment variable does to that plane.
| Variable | What it does | Default |
|---|---|---|
DATA_DIR |
Where the database, blobs and master key live | ./data |
DB_DRIVER |
sqlite | postgres | mysql
|
sqlite |
DATABASE_URL |
Connection string when DB_DRIVER isn't sqlite
|
(empty) |
KEY_PROVIDER |
file (raw master key on disk) | passphrase (wrapped) |
file |
LISTEN_ADDR |
Address the HTTP server binds | :8080 |
| Variable | What it does | Default |
|---|---|---|
BASE_URL |
Public URL — OAuth redirect, WebSocket origin, PWA manifest | (empty) |
BASE_PATH |
Serve the app under a subpath, e.g. /mail
|
/ |
AUTH_MODE |
passphrase | users | none
|
passphrase |
TRUST_PROXY |
Trust X-Forwarded-* from recognised proxy hops |
false |
TRUSTED_ORIGINS |
Extra CORS/CSRF-allowed origins, comma-separated | (empty) |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
|
Your own Google OAuth app, for Gmail sign-in | (empty) |
MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET
|
Your own Azure app, for Outlook sign-in | (empty) |
Register two redirect URIs on each OAuth application, not one:
<BASE_URL>/api/oauth/callbackfor the ordinary flow, andhttp://localhost:8642for the manual paste flow that instances without a public HTTPS address rely on. Both are shown with a copy button under Settings ▸ About ▸ Setup health ▸ OAuth. The second is not a setting and has no environment variable — nothing listens on it, and it is only ever a string the provider has to recognise. See Accounts. For the console-by-console steps to register the application itself at Google and Microsoft, see Register the OAuth application.
| Variable | What it does | Default |
|---|---|---|
SKIMMAIL_ALLOW_AUTH_NONE |
Required, in addition to auth.mode=none, before authentication can actually be disabled |
false (1/true to enable) |
SKIMMAIL_SKIP_CLAIM |
Skip the first-run claim-code gate (see Security) | false |
TRUSTED_PROXIES |
Comma-separated CIDRs trusted as X-Forwarded-For hops |
(empty = loopback/private only) |
| Variable | What it does | Default |
|---|---|---|
AUTO_LOCK_MINUTES |
Idle minutes before the client screen-locks; 0 = never |
0 |
SESSION_TTL_HOURS |
Login session lifetime; 0 = never expires |
720 (30 days) |
BACKGROUND_POLL_INTERVAL |
Poll-fallback period per account | 5m |
MAX_CONCURRENT_SYNCS |
Mailboxes syncing at once | 4 |
SYNC_RATE_PER_MIN |
Max sync starts per minute | 10 |
SYNC_DEPTH_DAYS |
Header sync window in days; 0 = full history |
30 |
SYNC_MAX_RETRIES |
Consecutive failures before auto-stopping an account's sync; 0 = never |
3 |
CACHE_MAX_SIZE_MB |
Evict cached message bodies above this total; 0 = no ceiling |
2048 |
BODY_TTL_DAYS |
Drop cached message bodies older than this; 0 = never expire |
90 |
CLIENT_IDLE_GRACE |
No-client window before idle-downscale | 30m |
IDLE_DOWNSCALE_WHEN_NO_CLIENT |
Downgrade IDLE→poll when no browser is open | false |
MAX_IDLE_CONNECTIONS |
Cap on concurrent IMAP IDLE connections; 0 = no cap |
0 |
LOG_LEVEL |
debug | info | warn | error
|
info |
LOG_MAX_SIZE_MB |
Rotate the log file at this size | 10 |
LOG_MAX_BACKUPS |
Rotated log files to keep | 3 |
CACHE_MAX_SIZE_MB and BODY_TTL_DAYS are Plane 2 in the strict sense: the
environment supplies the initial default, and a value saved from Settings
▸ Security ▸ Message body cache — or through PUT /api/settings/cache — is
stored in the database and wins from then on. Note that this is the reverse of
Plane 1, where setting the variable locks the field: here the environment
yields to a saved value, which is why that panel shows no "the environment
owns this" hint.
The panel has existed since 1.11.1. On 1.11.0 it was built but placed inside the hidden Storage tab, so no install could open it and the variables really were the only way. See Message body cache.
| Variable | What it does | Default |
|---|---|---|
MAX_CONNS_PER_ACCOUNT |
Concurrent IMAP connections per account (clamped 1–14) | 10 |
BLOB_ENCRYPT |
Encrypt cached message bodies at rest (AES-256-GCM, key derived from the master key). Reads decrypt regardless, so turning it off never strands an existing cache. On unless you explicitly turn it off: only false, 0, no and off disable it (case-insensitive, surrounding spaces ignored). Every other value — 1, TRUE, yes, on, or a typo — leaves encryption on
|
true |
LOG_FILE |
Path to a rotating log file; empty = stdout/journal only | (empty; apt sets /var/log/skimmail/skimmail.log) |
LOG_FORMAT |
text (key=value) | json
|
text |
FIREBASE_ENABLED / FIREBASE_CREDENTIALS
|
Optional mobile push via Firebase (FCM); most self-hosters don't need this — browser Web Push works with no setup |
false / (empty)
|
VAPID_PUBLIC / VAPID_PRIVATE / VAPID_SUBJECT
|
Override the auto-generated Web Push keypair | (empty — a keypair is generated and stored automatically) |
UPDATE_FEED_URL |
Override the self-update release feed (air-gapped mirrors) | (empty = GitHub Releases) |
UPDATE_PUBKEY |
The minisign public key that verifies self-update and plugin-manifest signatures. Official builds already have it linked in; set this only on a custom build | (empty on a custom build = notify-only updates, unsigned plugin manifest) |
REVOCATION_FEED_URL |
Override where the revoked-license-id list is fetched from | (empty = default feed) |
SKIMMAIL_PLUGINS_URL |
Override where plugins.json (the Remote access / WireGuard engine plugin catalog) is fetched from |
(empty = the default GitHub Pages manifest) |
BLOB_ENCRYPT is the only default-on boolean in this table, and it is the
only one that treats an unrecognised value as "on". Every other switch here
defaults to off, where refusing to understand a value is the harmless
direction; a default-on flag inverts that, so it fails towards still
encrypting.
If you run 1.11.0, check this one. That release compared the value against the exact word
true, soBLOB_ENCRYPT=1,=yes,=onand=TRUEall read as off and silently stopped encrypting new bodies. Fixed in 1.11.1. Bodies cached while it was off are not rewritten in place; they stay unencrypted until they leave the cache. Lowering the size or age limit in Settings ▸ Security pushes them out, and they come back encrypted.
AI tools are the one feature area compiled into the binary whose Settings tab is still hidden in 1.15.0 — their environment variables are intentionally left out of this table until the tab is turned back on. The Storage engine / S3 blob switch used to be hidden alongside them and became visible in 1.14.0, so its settings live in Settings ▸ Storage now. The body-cache knobs above shared the same fate earlier; since 1.11.1 their panel is in Settings ▸ Security instead. See Home for what is hidden and why.
Everything Plane 1 offers in the browser, skimmail config offers on the
host — plus the one thing the browser can't do once a credential exists:
change the sign-in mode.
skimmail config list # every setting, its value, and where it came from
skimmail config get base_url # one value, bare, for scripting
skimmail config set base_url https://mail.example.com
skimmail config unset base_url # clear it
skimmail config export # the env block for what's currently in the databaselist prints Plane 1 settings and the Plane 0 bootstrap keys in two separate
tables (a bootstrap change needs a restart to take effect; a Plane 1 change
doesn't, and the command says so). export skips secrets — they're
encrypted at rest and cannot be read back out; it prints a comment telling
you to re-paste them instead. Trying to set a key the environment already
owns fails with the same "remove it there first" message the browser shows.
This is also the way back in if you ever lock yourself out of the sign-in
mode from the browser, and the only place auth.mode can be changed once an
account exists.
bootstrap.env is deliberately narrow — DB_DRIVER, DATABASE_URL,
KEY_PROVIDER and LISTEN_ADDR, no more (DATA_DIR can never be among
them: it's where the file itself lives) — and Plane 1 never gets a file of
its own alongside the database. Giving the same setting
two independently-writable homes (a file and a database row, both claiming
to be authoritative) is exactly how another self-hosted project ended up with
an admin UI that silently can't save changes because a config file the UI
doesn't know about is quietly winning every time. SkimMail's rule stays
simple because it only has to hold in one direction: environment beats
everything, and everything else lives in exactly one place.
SkimMail · skimmail@base101.app · 2026-09-18 · commit f525934