Add DATABASE_POOL_SIZE / DATABASE_MAX_OVERFLOW / DATABASE_POOL_TIMEOUT,
consumed like every other typed env var through the pydantic Settings
(constrained Fields, defaults 5/10/30 matching SQLAlchemy's own baseline
so leaving them unset is behaviour-neutral). An out-of-range or
non-integer value fails validation and refuses to boot — the same
fail-loud behaviour a malformed DATABASE_URL already has — rather than
silently starting up misconfigured. create_db_engine sizes the pool from
these and logs the effective values at startup so they can be confirmed
from the boot output during an incident. In-memory SQLite (StaticPool,
which rejects the pool kwargs) is detected and built without them.
pool_pre_ping is deliberately not exposed: the default backend is a local
SQLite file with no network peer to drop idle connections, so it would add
a SELECT 1 per checkout for no benefit — and it detects dead connections,
not the live-but-wedged ones behind the exhaustion this series addresses.
These knobs are infrastructure the node needs before it can open a DB
session, so they can never be sourced from the DB (chicken-and-egg). A new
ENV_ONLY_FIELDS set keeps them out of the persisted settings blob and
stops a DB value from shadowing env in both SettingsService.initialize and
.update, so env stays authoritative.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ways a stale legacy NSEC could override or resurrect an nsec the
vault already owns (issue #553):
- initialize() re-applied env/blob values onto live settings after
bootstrap had decrypted the authoritative nsec, so a stale NSEC left
in .env would clobber it on restart (e.g. after rotating the key in
the admin UI). _apply_to_live_settings now never re-applies secret
fields; bootstrap_secrets is their only writer.
- An empty encrypted_nsec could not distinguish "never migrated" from
"intentionally cleared", so clearing the identity via the admin API
and restarting re-imported the old NSEC from env/blob. Record vault
ownership in a new secrets.nsec_managed column (set on legacy import
and on every set_nsec write); bootstrap skips the legacy import once
the vault owns the nsec, so a cleared identity stays cleared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
upstream_api_key was added to SECRET_FIELDS, so it was stripped from every
blob write — but unlike nsec, nothing migrates it into encrypted storage. A
node carrying it only in the DB blob would load it into memory once, rewrite
the blob without it, and lose it on the next restart, breaking upstream auth.
It is node-scoped config that really belongs on a provider, not a vault
secret, and it has no encrypted home yet. Remove it from SECRET_FIELDS so it
stays in the blob exactly as before; redaction on read and ignore-on-write in
the admin settings endpoint are unchanged. Encrypting it is follow-up work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persist and load admin password and nsec from the encrypted Secret store on
boot: generate a temporary admin password on first run (logged once), encrypt
a provided nsec, and fail fast if a stored nsec cannot be decrypted with the
current key. Stop clobbering live secret settings with empty env values.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>