# What officer-setup writes. Everything below this block is optional, or is on its way out. PORT=9000 POSTGRES_URL="postgres://postgres:password@localhost:5432/officer" # ── No secrets live here ─────────────────────────────────────────────────────────────────────── # JWT_SECRET and VAULT_STORE_KEY were here until 2026-08-13. Every encryption and signing key now # lives in the secret store — a 0600 SQLite file at $OFFICER_ROOT/secrets/officer-keys.db, one key # per purpose, created on first use. See docs/secret-store.md. # # The reason is blast radius rather than secrecy: bun auto-loads this file into ALL of the pm2 # processes, so a key here is readable from /proc//environ of twenty processes that mostly have # no business with it — officer-music held the key that decrypts wallet seed envelopes. # # BACK UP THAT FILE. Losing it signs everyone out and makes every encrypted column in Postgres # unreadable, and for the wallet seed that is unrecoverable. # ── Optional ─────────────────────────────────────────────────────────────────────────────────── # Where Officer is reached from a browser. Read by the task API host check and the CalDAV iOS profile # builder — the latter is the only thing that hard-requires it, and it demands https. # PUBLIC_URL=https://officer.example.com # Guards (CORS origin checks, rate limits, password-strength rules) are ON unless this is set to # "dev" or "development". Unset is hardened, which is why officer-setup no longer writes it — set it # by hand, on a local machine you trust, to develop. Note that `bun dev` does NOT set it: that script # only loads this file, so `bun dev` against a production .env runs fully hardened. # PUBLIC_BUILD_ENV=dev # DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR were here until 2026-08-12 and are no longer read. # The install root is derived as the parent of the working directory (src/servers/data-path.ts), so # data/, capabilities/ and dockers/ follow from it; the owner's home comes from the OS. Three values # that had to agree with each other and with the disk became one that cannot disagree. # ── Sidecars ──────────────────────────────────────────────────────────────────────────────────── # Each sidecar owns its upstream's credentials; the platform API is only a thin auth+forward proxy # and never sees them. An unset upstream URL is not fatal — the sidecar logs a warning at boot and # answers 503 until it is set, so you can run Officer with any subset of these configured. # Transmission (officer-transmission) is configured from the app, not from here — Transmission → # Connection. The daemon URL, the optional RPC auth and the RPC path live in `service_connections`, # with the password encrypted, so nothing outside the sidecar can read it. # InvoiceShelf (officer-invoiceshelf) is configured from the app, not from here — Invoices → Connection. # Instances, their Sanctum tokens and the company each one is pinned to live encrypted in # `invoiceshelf_accounts`, so nothing outside the sidecar can read a token. # slskd (officer-slskd) is configured from the app, not from here — Soulseek → Connection. The # daemon URL and its API key live encrypted in `service_connections`; the sidecar injects the key as # X-API-Key on every forwarded request. # Vaultwarden (officer-vault). VAULT_STORE_KEY is at the top of this file — it is the platform's # key, not Vaultwarden's, however much the name and its old position here suggested otherwise. # VAULTWARDEN_URL=http://127.0.0.1:8222 # The Anthropic proxy (officer-anthropic-proxy) binds PORT + 1, derived rather than configured — see # src/servers/officer-url.mjs. There is nothing to set. It holds no credential from this file either: # the upstream token is the OAuth one `claude` writes to ~/.claude/.credentials.json, and the # ANTHROPIC_API_KEY the agent presents to it is the proxy's own generated secret. # ReClip — the self-hosted yt-dlp service the download-media capability talks to. Defaults to # http://localhost:8899. # RECLIP_URL=http://localhost:8899 # ── Headscale (/api/vpn) ──────────────────────────────────────────────────────────────────────── # These drive the /api/vpn router, NOT the officer-headscale sidecar. The sidecar deliberately reads # neither, keeping its registered servers and their keys in Postgres so host env can never shadow # one. Set these only if you use /api/vpn. # HEADSCALE_URL=https://headscale.example.com # HEADSCALE_API_KEY="" # HEADSCALE_USER=officer # ── Bitcoin wallet (officer-wallet) ───────────────────────────────────────────────────────────── # The chain data source is NOT here — it is configured from the app, at Wallet → Settings → Chain # source, and stored per owner. Any Esplora-compatible API works (electrs, esplora, mempool.space); # it defaults to the public mempool.space until you set one. # WALLET_NETWORK=bitcoin # bitcoin | testnet | signet | regtest # # How long an unlocked wallet stays unlocked, in seconds. Default 900 (15 min). The root key is held # in the sidecar's memory for exactly this long after an unlock, then wiped. Shorter is safer. # WALLET_UNLOCK_TTL_SEC=900 # # NOTE: seed material is encrypted with VAULT_STORE_KEY (above) on top of the owner passphrase that # seals it. Both are required to spend. If you lose VAULT_STORE_KEY, every stored seed is # unrecoverable — back up the mnemonics separately, offline. # Immich (officer-photos) is configured from the app, not from here — Photos → Connection. Instances and # their API keys live encrypted in `photos_config`, so the platform never sees a key.