Files
platform/.env.example
T
pastilhasandClaude Opus 5 035a1ba8f6 add photos, an immich-backed library behind its own sidecar
officer-photos owns the whole Immich contract: the instance URL and the API
key live there and nowhere else, and the platform side is an auth-gated
forwarder holding no credentials. The route surface is an allow-list keyed on
the first path segment, so admin, auth, api-keys, sessions, jobs, system-config
and libraries are unreachable by construction rather than by enumeration.

The UI mirrors Immich's own sidebar — timeline, explore, map, search, albums,
people, favorites, sharing, archive, trash — because the point of a sidecar
screen is to reproduce what the upstream already ships, then extend it. The
timeline reads Immich's columnar time-bucket format directly; selection lives
in the URL per docs/navigation-audit.md.

Two things worth knowing for anyone touching this later:

- `duration` is an integer count of milliseconds in Immich 3.0. It was an
  HH:MM:SS.mmm string before, and every stale example still shows that form.
- the map container is sized with h-full/w-full, never `absolute inset-0`.
  maplibre's stylesheet sets `position: relative; overflow: hidden` on the
  element it is given, and an unlayered vendor rule beats Tailwind 4's layered
  `.absolute` regardless of source order — so the div collapses to height 0 and
  clips its own canvas away. Nothing errors: the GL context is healthy, tiles
  download and pixels are drawn into a buffer nobody ever composites.

maplibre-gl is pinned to 5.x deliberately; 6.0 resolves a separate worker file
from import.meta.url, which Officer's index.html fallback answers with HTML.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 00:18:33 +00:00

87 lines
5.1 KiB
Bash

PORT=9000
JWT_SECRET="<generate with: openssl rand -base64 32>"
POSTGRES_URL="postgres://postgres:password@localhost:5432/officer"
MAIL_TRANSPORT="smtp://localhost:1025"
PUBLIC_URL=http://localhost:9000
# Guards (CORS origin checks, rate limits, password-strength rules) are ON unless this is set to
# "dev" or "development". Leave it unset or set it to "production" for a real deployment; only set
# it to "dev" on a local machine you trust, since that disables all three.
PUBLIC_BUILD_ENV=production
DATA_PATH=/path/to/data
OFFICER_ITEMS_DIR=/path/to/officer-items
HOME_DIR=/home/user
BROWSER_RELAY_PORT=18792
# ── 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). TRANSMISSION_USER/PASS are only needed if the daemon has RPC
# auth turned on; leave them empty otherwise, since Transmission rejects an empty Basic header.
# TRANSMISSION_RPC_PATH defaults to /transmission/rpc and only needs setting behind a reverse proxy
# that mounts the RPC endpoint somewhere else.
TRANSMISSION_URL=http://127.0.0.1:9091
TRANSMISSION_USER=
TRANSMISSION_PASS=
# TRANSMISSION_RPC_PATH=/transmission/rpc
# InvoiceShelf (officer-invoiceshelf). The token is a Sanctum personal access token — mint one with
# POST /api/v1/auth/login {username: <email>, password, device_name} and copy the `token` field. It has
# full abilities and never expires, so treat it as a password.
# INVOICESHELF_COMPANY_ID pins which company every request is scoped to. Leave it unset on a
# single-company install and the sidecar resolves it once at boot and LOGS the choice — worth setting
# explicitly if you have more than one, because InvoiceShelf does not error on a wrong company header,
# it silently returns the other company's data.
INVOICESHELF_URL=https://invoice.example.com
INVOICESHELF_TOKEN="<sanctum api token, e.g. 1|xxxxxxxx>"
# INVOICESHELF_COMPANY_ID=1
# slskd (officer-slskd). The key is injected as X-API-Key on every forwarded request.
SLSKD_URL=http://127.0.0.1:5030
SLSKD_API_KEY="<slskd api key>"
# Vaultwarden (officer-vault). VAULT_STORE_KEY encrypts stored secrets at rest — any strong secret
# of 16+ chars works, and CHANGING IT MAKES EXISTING STORED SECRETS UNREADABLE.
VAULTWARDEN_URL=http://127.0.0.1:8222
VAULT_STORE_KEY="<generate with: openssl rand -base64 32>"
# Anthropic proxy (officer-anthropic-proxy). Defaults to 5051; it holds the API credential, which
# lives in the host env rather than here.
# ANTHROPIC_PROXY_PORT=5051
# 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 admin api key>"
# HEADSCALE_USER=officer
# ── Bitcoin wallet (officer-wallet) ─────────────────────────────────────────────────────────────
# Chain data source for the self-custodial on-chain wallet. Any Esplora-compatible API works —
# mempool.space by default, or point it at your own node's esplora/electrs when you run one.
# WALLET_ESPLORA_URL=https://mempool.space/api
# 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). The key is injected as x-api-key on every forwarded request; the platform never
# sees it. Create it in Immich: Account Settings → API Keys → New API Key. Immich keys are SCOPED — grant
# all permissions unless you want a read-only library, because a missing permission answers 403 on that one
# route and looks like a broken feature rather than a bad credential.
IMMICH_URL=http://127.0.0.1:2283
IMMICH_API_KEY="<immich api key>"