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()=>{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: … printedyesno
sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
sys_account rows1 (provider_id: credential)0
POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    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()=>{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: … printedyesno
    sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
    sys_account rows1 (provider_id: credential)0
    POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      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()=>{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: … printedyesno
      sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
      sys_account rows1 (provider_id: credential)0
      POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      Labels

      Type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        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()=>{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: … printedyesno
        sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
        sys_account rows1 (provider_id: credential)0
        POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

        Activity

        Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

        Metadata

        Metadata

        Assignees

        Labels

        Type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          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()=>{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: … printedyesno
          sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
          sys_account rows1 (provider_id: credential)0
          POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          Labels

          Type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            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()=>{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: … printedyesno
            sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
            sys_account rows1 (provider_id: credential)0
            POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

            Activity

            Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

            Metadata

            Metadata

            Assignees

            Labels

            Type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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()=>{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: … printedyesno
              sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
              sys_account rows1 (provider_id: credential)0
              POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              Labels

              Type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                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()=>{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: … printedyesno
                sys_user rows1 (Dev Admin, admin@objectos.ai)13 (none with an email that can log in)
                sys_account rows1 (provider_id: credential)0
                POST /api/v1/auth/sign-in/email as admin@objectos.ai200401 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.

                Activity

                Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                Metadata

                Metadata

                Assignees

                Labels

                Type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions