Installer seeds bootstrap admin password; Postgres parameter-type bug fixed

Two commits. The installer now asks for a `BOOTSTRAP_ADMIN_PASSWORD` so the operator can sign in immediately after deploy without going through the signup form. The follow-up fixes a silent Postgres transaction abort that prevented the admin account from ever being created.

infrastructuresecurity

Installer change (fcc68098, +121/-4 across 4 files). The wizard gains a step asking whether to auto-generate or manually type a password for the bootstrap admin email. The value lands in .env.compose as BOOTSTRAP_ADMIN_PASSWORD and in the secrets backup. The post-install report surfaces it so the operator can sign in at /login directly.

ensureBootstrapAdmin() in backend/src/lib/users.ts is extended: if the bootstrap email doesn't yet exist in the database AND BOOTSTRAP_ADMIN_PASSWORD is set to at least 12 characters, it creates the user with that password (bcrypt, cost 12), role = admin, status = active. onboarded_at is left NULL - first sign-in still triggers the onboarding wizard even though the account exists. Passwords shorter than 12 characters are refused with a warning log, not a crash. The legacy path (sign up via the form, get auto-promoted) still works when the password env var is absent.

The bug (35e409f6, +6/-2 in 1 file). The audit_events insert inside ensureBootstrapAdmin reused $1 for both user_id (uuid) and target_id (text):

VALUES ($1, 'user.bootstrap', 'user', $1, $2::jsonb)

Postgres infers the type of $1 from the first use - uuid - and then rejects the second use where text is expected. The whole transaction rolled back silently. The fix separates the parameters with explicit casts:

VALUES ($1::uuid, 'user.bootstrap', 'user', $2::text, $3::jsonb)

passing the same id value twice with different positions. Without this fix, the bootstrap admin was never created on fresh installs, and the operator's first sign-in returned "Invalid email or password" with no diagnostic.

So what The password-seeding pattern is worth adopting if you use the broader installer flow. The Postgres type-inference bug is the more transferable lesson: `pg` infers parameter types from first use, and reusing `$N` across columns of different types silently aborts the transaction. A grep for multi-use positional parameters in your own parameterised inserts is cheap insurance. The `onboarded_at = NULL` trick - pre-creating the account while still routing first sign-in through the onboarding wizard - is also a clean pattern for bootstrapped admin accounts.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

2 commits from cpatpa/PIP, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
fcc68098 Installer + backend: seed the bootstrap admin password Claude 2026-05-16 ↗ GitHub
commit body
The installer now asks during the wizard whether to auto-generate
or type a password for the BOOTSTRAP_ADMIN_EMAIL account. The value
is written to .env.compose as BOOTSTRAP_ADMIN_PASSWORD (and into
the secrets-backup file), and surfaced in the post-install final
report so the operator can sign in immediately at /login without
going through the signup form first.

Backend ensureBootstrapAdmin() now creates the user with the
supplied password (hashed via bcrypt, cost 12) if the email doesn't
yet exist. Password must be >= 12 chars or the seeding step is
skipped. onboarded_at is left null so the first sign-in still goes
through the onboarding wizard.

The legacy "sign up via the form, get auto-promoted to admin"
behaviour remains intact when BOOTSTRAP_ADMIN_PASSWORD is empty.
35e409f6 Fix ensureBootstrapAdmin: cast $1 to uuid and target_id to text Claude 2026-05-16 ↗ GitHub
commit body
The audit_events insert in the bootstrap path was reusing $1 for
both user_id (uuid) and target_id (text), which Postgres rejected
with "inconsistent types deduced for parameter $1". The whole
transaction rolled back, so the bootstrap user never got created
on fresh installs and the operator hit "Invalid email or password"
on first sign-in with no other diagnostic.

Use two separate parameters with explicit casts. The metadata cast
to jsonb stays.

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-370.md from inside the repo you want the changes in.

⬇ Download capture-thread-370.md