Skip to content

Configuration

SkimMail docs edited this page Sep 18, 2026 · 8 revisions

English · Tiếng Việt · 中文

Configuration

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.

The three planes

Plane 0 — Bootstrap

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_DIR is 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 via skimmail config set. This file exists specifically so that someone who installed with a one-line curl | sh and wants Postgres has somewhere to put the DSN without editing a systemd EnvironmentFile by 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.

Plane 1 — Instance configuration

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 list can 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 to none additionally requires SKIMMAIL_ALLOW_AUTH_NONE=1 in 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.

Plane 2 — App settings

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).

Environment variable reference

"Plane" says where a saved change (if any) is kept; "env is…" says what the environment variable does to that plane.

Plane 0 — bootstrap

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

Plane 1 — instance configuration (env locks the field)

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/callback for the ordinary flow, and http://localhost:8642 for 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.

Env-only security gates (no lock, no override — always the environment)

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)

Plane 2 — app settings (env is the initial default 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.

Boot-only, no UI or CLI override at all

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, so BLOB_ENCRYPT=1, =yes, =on and =TRUE all 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.

The shell escape hatch: skimmail config

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 database

list 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.

What must never become a config file

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

Clone this wiki locally