Skip to content

maybeSeedDevAdmin gates on "any sys_user row", but runs at kernel:ready — so any app that seeds people permanently loses the objectstack dev login #14157

Description

@os-warren

Measured on @objectstack/plugin-auth 17.2.0 / @objectstack/cli 17.2.0 against a real objectstack dev boot, twice, with and without an app seed. Not a race — a deterministic ordering.

What happens

objectstack dev promises a loginable dev admin on an empty DB:

--seed-admin … Seed a known, loginable dev admin (admin@objectos.ai / admin123) in-process via the runtime on an EMPTY DB, then promote it to platform admin. Default: on (idempotent — only acts on a zero-user DB, never overwrites an existing account).

An app that declares any sys_user rows in defineStack({ data }) makes the DB non-zero-user before that check runs, so the admin is never created — and because the check is "any human exists", it is never created on any later boot either. The deployment ends up with no loginable account at all.

Why it is ordering and not a race

AuthPlugin registers the seeding on kernel:ready:

ctx.hook("kernel:ready", async () => {
  await this.maybeSeedDevAdmin(ctx);
});

kernel:ready fires after every plugin has started. The app's declarative seed runs inside AppPlugin.start() — awaited there against OS_INLINE_SEED_BUDGET_MS — so it always completes first. Plugin registration order does not help: serve.ts mounts AuthPlugin at step 5d and the app's plugins afterwards, and the observed outcome is the same either way, because the phases are what order these two, not use().

The gate itself:

const rows = await ql.find(SystemObjectName.USER, { where: {}, limit: 50 }, { context: { isSystem: true } });
const humans = rows.filter((u) => u && u.id !== SystemUserId.SYSTEM && u.role !== "system");
if (humans.length > 0) { /* skipped */ }

A seeded person is a directory row with no credential. It is not a login, and treating it as one is the defect.

Measurement

Same app (objectstack-ai/duly), same command, two trees. Baseline is the app with an empty data: []; the other seeds a 13-person demo org.

baseline (data: []) with a 13-person sys_user seed
🔑 Dev admin: … printed yes no
sys_user rows 1 (Dev Admin, admin@objectos.ai) 13 (none with an email that can log in)
sys_account rows 1 (provider_id: credential) 0
POST /api/v1/auth/sign-in/email as admin@objectos.ai 200 401 INVALID_EMAIL_OR_PASSWORD
GET /api/v1/auth/bootstrap-status {"hasOwner": true}

That last row is what makes it a dead end rather than an inconvenience: hasOwner: true also tells the console there is already an owner, so no first-admin flow is offered. Self-registration still works (disableSignUp: false), but the account created that way is role: "user" and is not promoted, so it lands in an app it cannot read.

Reproduction

git clone https://github.com/objectstack-ai/duly && cd duly && pnpm install
pnpm dev            # ✅ prints "🔑 Dev admin: admin@objectos.ai / admin123"; login works
# now add any sys_user rows to dulySeeds in src/data/index.ts, on a FRESH db:
rm -rf .objectstack/data && pnpm dev
# ❌ no dev-admin line, sys_account empty, sign-in 401

Suggested fix

Gate on a credential-bearing account, not on the existence of user rows. Either is enough on its own:

  • ask sys_account (provider_id: 'credential') whether any local login exists; or
  • ask specifically whether the configured OS_SEED_ADMIN_EMAIL already resolves to a user with an account — which is the question maybeReportExistingSeedAdmin already asks two functions later, so the shape is right there.

bootstrap-status's hasOwner looks like it wants the same treatment, for the same reason.

A workaround exists but is not one an evaluator will guess: boot once with the app's seed absent so the admin is minted, then boot normally. Verified — with the admin pre-existing, the same app seed loads all 459 rows, leaves the account's email and credential untouched, and the demo works end to end.

Why this is worth fixing rather than documenting

Demo/onboarding seeds are exactly the apps that seed people, and objectstack dev on a clean checkout is exactly the moment an evaluator forms their opinion. The failure is silent on the seed side (inserted: 459, errored: 0), and the only symptom is a login page that rejects the credentials the CLI's own --help documents.

Found while building the demo seed for objectstack-ai/duly#7.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions