feat: create the initial platform owner from an operator command #34

Merged
Zayin_Admin merged 1 commits from phase-7b/initial-owner-bootstrap into main 2026-08-24 03:44:19 +00:00
Owner

A fresh production installation deadlocked. ADR 0034 gave Ekklesia a login
page and ADR 0019 gave it a single-use platform-owner bootstrap, but that
bootstrap grants ownership to a user id that already exists and has no way
to bring one into being. Public signup is disabled by default and
production cannot enable it at all, so a newly deployed instance had a
login page, no account, and no supported way to create one — only hand-
written SQL or briefly opening public registration.

pnpm identity:create-initial-owner -- --email [email protected]

creates the identity and grants it ownership, in the same shape as the
other operator commands: trusted local tooling, reachable only by someone
who already holds the database credentials, serving no HTTP route. No
/signup, /register, or /setup route is added, no bootstrap endpoint, and
no setup token; public signup stays disabled in every environment and a
test asserts a freshly bootstrapped deployment still refuses
/api/auth/sign-up/email.

The identity is created through Better Auth's own sign-up path, so
hashing, salting, normalization and the 8-128 bound are the identity
system's rather than a second implementation that could drift. Because the
server's instance refuses account creation, the command builds its own
in-process instance; nothing in apps/api imports it and the running
server's posture is untouched.

The sign-up result is verified rather than trusted. Better Auth returns a
fabricated success for an already-registered address — a user object whose
id was never persisted, database unchanged. That is correct enumeration
defence on a public endpoint and a trap here, so the identity is always
read back from the database and that read, not the response, is what the
grant uses.

An existing usable identity is adopted: ownership is granted and its
password left alone, reported as an adoption rather than a creation. This
is the recovery path for the one partial failure possible, since sign-up
commits on Better Auth's connection and the grant is a separate
transaction. A credential-less row is refused instead — making it an owner
would consume the single use on an identity nobody can authenticate as. A
password is never reset and an account is never taken over.

Whole invocations are serialized by an advisory lock on their own key, so
a race cannot leave a stray signable-in account behind; the grant's own
locked single-use transaction is unchanged. There is no --force or reset.

The password is prompted for invisibly or read from stdin, never from
argv (--password is refused with an explanation) and never from an
environment variable, so nothing about it becomes permanent deployment
configuration. The command needs DATABASE_URL and nothing else — verified
that sign-up needs no secret, base URL, or trusted origins, and that the
resulting account signs in through a separately configured server — so the
first owner can be created before TLS or DNS exist. It never migrates.

Proven end to end rather than at the database: a fresh installation is
bootstrapped, signed in through the real browser login transport with
signup disabled, its principal read, and used to open an owner-protected
resource that stays closed to anonymous callers.

Co-Authored-By: Claude Opus 5 [email protected]

A fresh production installation deadlocked. ADR 0034 gave Ekklesia a login page and ADR 0019 gave it a single-use platform-owner bootstrap, but that bootstrap grants ownership to a user id that already exists and has no way to bring one into being. Public signup is disabled by default and production cannot enable it at all, so a newly deployed instance had a login page, no account, and no supported way to create one — only hand- written SQL or briefly opening public registration. pnpm identity:create-initial-owner -- --email [email protected] creates the identity and grants it ownership, in the same shape as the other operator commands: trusted local tooling, reachable only by someone who already holds the database credentials, serving no HTTP route. No /signup, /register, or /setup route is added, no bootstrap endpoint, and no setup token; public signup stays disabled in every environment and a test asserts a freshly bootstrapped deployment still refuses /api/auth/sign-up/email. The identity is created through Better Auth's own sign-up path, so hashing, salting, normalization and the 8-128 bound are the identity system's rather than a second implementation that could drift. Because the server's instance refuses account creation, the command builds its own in-process instance; nothing in apps/api imports it and the running server's posture is untouched. The sign-up result is verified rather than trusted. Better Auth returns a fabricated success for an already-registered address — a user object whose id was never persisted, database unchanged. That is correct enumeration defence on a public endpoint and a trap here, so the identity is always read back from the database and that read, not the response, is what the grant uses. An existing usable identity is adopted: ownership is granted and its password left alone, reported as an adoption rather than a creation. This is the recovery path for the one partial failure possible, since sign-up commits on Better Auth's connection and the grant is a separate transaction. A credential-less row is refused instead — making it an owner would consume the single use on an identity nobody can authenticate as. A password is never reset and an account is never taken over. Whole invocations are serialized by an advisory lock on their own key, so a race cannot leave a stray signable-in account behind; the grant's own locked single-use transaction is unchanged. There is no --force or reset. The password is prompted for invisibly or read from stdin, never from argv (--password is refused with an explanation) and never from an environment variable, so nothing about it becomes permanent deployment configuration. The command needs DATABASE_URL and nothing else — verified that sign-up needs no secret, base URL, or trusted origins, and that the resulting account signs in through a separately configured server — so the first owner can be created before TLS or DNS exist. It never migrates. Proven end to end rather than at the database: a fresh installation is bootstrapped, signed in through the real browser login transport with signup disabled, its principal read, and used to open an owner-protected resource that stays closed to anonymous callers. Co-Authored-By: Claude Opus 5 <[email protected]>
Zayin_Admin added 1 commit 2026-08-24 03:41:39 +00:00
feat: create the initial platform owner from an operator command
CI / verify (pull_request) Successful in 41s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 1m18s
c8fa13dd6a
A fresh production installation deadlocked. ADR 0034 gave Ekklesia a login
page and ADR 0019 gave it a single-use platform-owner bootstrap, but that
bootstrap grants ownership to a user id that already exists and has no way
to bring one into being. Public signup is disabled by default and
production cannot enable it at all, so a newly deployed instance had a
login page, no account, and no supported way to create one — only hand-
written SQL or briefly opening public registration.

    pnpm identity:create-initial-owner -- --email [email protected]

creates the identity and grants it ownership, in the same shape as the
other operator commands: trusted local tooling, reachable only by someone
who already holds the database credentials, serving no HTTP route. No
/signup, /register, or /setup route is added, no bootstrap endpoint, and
no setup token; public signup stays disabled in every environment and a
test asserts a freshly bootstrapped deployment still refuses
/api/auth/sign-up/email.

The identity is created through Better Auth's own sign-up path, so
hashing, salting, normalization and the 8-128 bound are the identity
system's rather than a second implementation that could drift. Because the
server's instance refuses account creation, the command builds its own
in-process instance; nothing in apps/api imports it and the running
server's posture is untouched.

The sign-up result is verified rather than trusted. Better Auth returns a
fabricated success for an already-registered address — a user object whose
id was never persisted, database unchanged. That is correct enumeration
defence on a public endpoint and a trap here, so the identity is always
read back from the database and that read, not the response, is what the
grant uses.

An existing usable identity is adopted: ownership is granted and its
password left alone, reported as an adoption rather than a creation. This
is the recovery path for the one partial failure possible, since sign-up
commits on Better Auth's connection and the grant is a separate
transaction. A credential-less row is refused instead — making it an owner
would consume the single use on an identity nobody can authenticate as. A
password is never reset and an account is never taken over.

Whole invocations are serialized by an advisory lock on their own key, so
a race cannot leave a stray signable-in account behind; the grant's own
locked single-use transaction is unchanged. There is no --force or reset.

The password is prompted for invisibly or read from stdin, never from
argv (--password is refused with an explanation) and never from an
environment variable, so nothing about it becomes permanent deployment
configuration. The command needs DATABASE_URL and nothing else — verified
that sign-up needs no secret, base URL, or trusted origins, and that the
resulting account signs in through a separately configured server — so the
first owner can be created before TLS or DNS exist. It never migrates.

Proven end to end rather than at the database: a fresh installation is
bootstrapped, signed in through the real browser login transport with
signup disabled, its principal read, and used to open an owner-protected
resource that stays closed to anonymous callers.

Co-Authored-By: Claude Opus 5 <[email protected]>
Zayin_Admin merged commit 3ce4fe03fe into main 2026-08-24 03:44:19 +00:00
Zayin_Admin deleted branch phase-7b/initial-owner-bootstrap 2026-08-24 03:44:19 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Zayin/Ekklesia#34