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()=>{awaitthis.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:
constrows=awaitql.find(SystemObjectName.USER,{where: {},limit: 50},{context: {isSystem: true}});consthumans=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.
Measured on
@objectstack/plugin-auth17.2.0 /@objectstack/cli17.2.0 against a realobjectstack devboot, twice, with and without an app seed. Not a race — a deterministic ordering.What happens
objectstack devpromises a loginable dev admin on an empty DB:An app that declares any
sys_userrows indefineStack({ 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
AuthPluginregisters the seeding onkernel:ready:kernel:readyfires after every plugin has started. The app's declarative seed runs insideAppPlugin.start()— awaited there againstOS_INLINE_SEED_BUDGET_MS— so it always completes first. Plugin registration order does not help:serve.tsmountsAuthPluginat 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, notuse().The gate itself:
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 emptydata: []; the other seeds a 13-person demo org.data: [])sys_userseed🔑 Dev admin: …printedsys_userrowsDev Admin,admin@objectos.ai)sys_accountrowsprovider_id: credential)POST /api/v1/auth/sign-in/emailasadmin@objectos.aiINVALID_EMAIL_OR_PASSWORDGET /api/v1/auth/bootstrap-status{"hasOwner": true}That last row is what makes it a dead end rather than an inconvenience:
hasOwner: truealso 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 isrole: "user"and is not promoted, so it lands in an app it cannot read.Reproduction
Suggested fix
Gate on a credential-bearing account, not on the existence of user rows. Either is enough on its own:
sys_account(provider_id: 'credential') whether any local login exists; orOS_SEED_ADMIN_EMAILalready resolves to a user with an account — which is the questionmaybeReportExistingSeedAdminalready asks two functions later, so the shape is right there.bootstrap-status'shasOwnerlooks 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 devon 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--helpdocuments.Found while building the demo seed for
objectstack-ai/duly#7.