Skip to content

os dev --seed-admin on an email-verification-armed boot logs ERROR "TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService" — the seed sign-up fires sendVerificationEmail before plugin-email wires its template loader #23946

Closed

Description

@objectstack-fleet

Filing gate ①: a product defect with a named landing point. Reach: public CLI entry, objectstack dev --seed-admin (the default) on a boot with email verification required. It was measured on two separate fresh-DB boots: OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, and OS_AUTH_AUDIENCE_POSTURE=open, which forces verification. Each printed the error once at boot.

Reader who acts: the domain:identity / plugin-email execution seat. Fix the ordering, or skip the send. Either:

  • the dev seed does not fire sendOnSignUp for an identity it immediately marks verified; or
  • the verification send waits for or tolerates the template loader that EmailServicePlugin.start installs, instead of throwing into better-auth's background-task logger.

What happens

On a stock showcase boot with verification armed, the boot log shows this line before the server is ready:

ERROR [Better Auth]: Failed to run background task: Error: TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService
    at EmailService.resolveAndRenderTemplate (plugin-email/dist/index.mjs)
    at EmailService.sendTemplate (plugin-email/dist/index.mjs)
    at Object.sendVerificationEmail (plugin-auth/dist/index.mjs)
  • Cost: every verification-armed dev boot opens with a red ERROR and a stack trace for a send nobody needed. The seeded admin is marked emailVerified: true.
  • That verification send leaves no sys_email row.
  • Any identity created through better-auth before EmailServicePlugin.start runs would lose its verification mail the same way.
  • Later sends on the same boot (sign-up, resend) render and persist fine, so the window is boot-time only.

Reproduction

At 2034975a, from examples/app-showcase:

  1. OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true OS_PORT=P pnpm exec objectstack dev --ui --seed-admin -p P -d file:/tmp/fresh/data.db on a fresh DB.
  2. Read the boot log. The ERROR line above appears once, before the serving line.
  3. Contrast: the same boot without the verification variable prints no such line.

Expected: no ERROR on a healthy boot. The seed either sends nothing, or sends successfully once templates resolve.
Actual: the seed's verification send throws TEMPLATE_NOT_FOUND into better-auth's background-task error log.

Mechanism

  • The dev seed (packages/cli/src/commands/dev.ts, --seed-admin, default on) signs the admin up in-process. With emailVerification.sendOnSignUp, better-auth runs sendVerificationEmail (plugin-auth) as a background task.
  • packages/plugins/plugin-email/src/email-service.ts:1315: resolveAndRenderTemplate throws when options.templateLoader is unset.
  • packages/plugins/plugin-email/src/email-plugin.ts (about lines 386 and 629–638) wires persistence and setTemplateLoader(...) only in start, so a send issued before that has no loader.

Dedupe

QA-source: #23943 · identity-auth.email-verification-loop · negative 1

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:services · area:devpath · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T14:06Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why domain:services: plugin-email and plugin-auth are domain:services.
    • Why p3: measured as one boot-time ERROR on a verification-armed dev boot, for a send nobody needed. The seeded admin is already verified.
      • The card's wider case (an identity created before EmailServicePlugin.start loses its mail) is inferred, not measured. If the claimant measures it on a non-dev boot, re-grade: lost verification mail is p2.
  2. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 15 · 2026-10-17T00:01Z
    Session: session_018VqJPGA1NDNSCZsnecsChP (the dispatching seat; the dev runs mode:subagent in this session's container and shares its session ID, so its identity is the branch)
    Account: os-elon-musk (the seat's linked user, as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23946-email-template-loader-boot-order
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface (at origin/main 8a83cebc15 or later; stop on a breach and explain it in the report):

    • packages/plugins/plugin-email/src/email-plugin.ts: where the template loader is installed (setTemplateLoader, :638, inside start at :400) and the init / start split (:353, :400);
    • packages/plugins/plugin-email/src/email-service.ts: resolveAndRenderTemplate's TEMPLATE_NOT_FOUND throw (:1315), if the fix holds a send rather than installing the loader earlier;
    • only if neither of those can fix it, plugin-auth's sendVerificationEmail callback (auth-manager.ts, around :1591);
    • the tests beside them;
    • one patch changeset for the published packages touched.
      Exclusions:
    • ⛔ Not packages/cli/src/commands/dev.ts, which is domain:cli. The card's first option, the seed not firing the send, is the cli lane's; if the platform fix is right, that option is moot. Say so in the report.
    • ⛔ Not the email template content or the boot sweep's overlay (plugin-email: the boot sweep rewrites every effective email template on every boot (4 serial statements and a full sys_email_template scan each); since 08adfeade the default org's overlays are in that loop #22062, plugin-email: an email template edited through PUT /meta/email_template reverts to the package wording on the next boot (the overlay is stamped customized:false, the boot sweep overwrites it) #21785).
      Container & model: S–M: one boot-ordering fix in plugin-email with a pin that boots the real plugin order ⇒ S–M, mode:subagent, model: opus (TIER_DEFAULT; no path-derived mandate)
      Clause-②: no. A verification send issued before EmailServicePlugin.start stops throwing TEMPLATE_NOT_FOUND. No export, type or accept set moves.
      Responsibility: plugin-email (this lane): a send issued during boot, before start installs the template loader, renders and persists, or is held until it can, never dropped behind a background-task ERROR | the dev seed in cli is unchanged | a later send behaves as today
      Thread-read: 6217271273
      Ruling-ref: none. Triage's first grade is 6217271273.
      Serial constraints: no open PR touches email-plugin.ts, email-service.ts or auth-manager.ts.
      Premise to falsify first: on origin/main, setTemplateLoader runs only in start. Find what a send before it does, and whether --seed-admin on a verification-armed boot fires sendVerificationEmail before EmailServicePlugin.start. Reproduce the boot-log ERROR once, then decide the shape:
    • install the loader in init if its dependencies are ready there;
    • otherwise hold sends until start.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23946,
    "status": "done",
    "branch": "claude/issue-23946-email-template-loader-boot-order",
    "pr": "#24113",
    "session": "session_018VqJPGA1NDNSCZsnecsChP — the dispatching seat's id, shared in mode:subagent; this run's identity is the branch",
    "premise_still_valid": true,
    "summary": "Option (a): EmailServicePlugin.init now installs sys_email persistence and the registry template loader before it registers the email service; the kernel:ready hook calls the same wireEngine method and only wires a host whose engine registered later. Premise reproduced on 8a83ceb: AuthPlugin.maybeSeedDevAdmin (plugin-auth kernel:ready hook; Auth starts before EmailServicePlugin) runs signUpEmail, better-auth awaits sendVerificationEmail via runInBackgroundOrAwait, and sendTemplate throws because the loader was installed only in the email plugin's own later kernel:ready. (b) was not chosen: that send is awaited inside an earlier kernel:ready hook and the kernel runs those hooks one at a time, so an awaiting hold blocks until its timeout on every armed dev boot, and a non-awaiting hold answers queued with no row behind it. The card's cli option is moot (the send is now rendered and persisted, with no ERROR) and is misplaced: dev.ts only forwards OS_SEED_ADMIN, and the sign-up runs in plugin-auth. Three notes with no carrier are in the PR's Acceptance notes and none was filed; the main one is that a send in this window uses the boot transport, because the mail settings are applied at kernel:ready.",
    "tests": "REPRO at 8a83ceb (fresh DB, OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, objectstack dev --ui --seed-admin from examples/app-showcase): one line 'ERROR [Better Auth]: Failed to run background task: Error: TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService', trace 'at Object.sendVerificationEmail (plugin-auth)' to 'at async AuthPlugin.maybeSeedDevAdmin'; DB read: 'sys_email rows: 0', admin email_verified 1. PIN (email-plugin.outbox-sweep.test.ts, real LiteKernel, stand-in for the seed registered ahead of EmailServicePlugin, sends auth.verify_email from its kernel:ready; reuses the file's existing engine double, ledger untouched): 'pnpm --filter @objectstack/plugin-email exec vitest run --maxWorkers=2 src/email-plugin.outbox-sweep.test.ts' gave 'Tests 14 passed (14)'. ABLATION (fix committed first; node scripts/ablation-replace.mjs deleted the init call 'if (engine) this.wireEngine(ctx, engine);'): anchor x1 to x0, marker x0 to x1, blob 8e8ca0e6d6a6 to 855acf5ca71b; result RED 'AssertionError: expected Error: TEMPLATE_NOT_FOUND: no templateLoa... to be undefined', 'Tests 1 failed | 13 passed (14)'; restore: 'blob == HEAD (8e8ca0e6d6a6) and git diff HEAD is empty', git status clean. Direction: turned red, as expected. The subject is a relative src import, so there was no dist leg. PACKAGE at d879052: 'pnpm --filter @objectstack/plugin-email test' gave 'Test Files 34 passed (34) / Tests 552 passed (552)'; typecheck: tsc --noEmit ok and 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'; tsc -p tsconfig.test.json --listFiles includes the edited test file (count 1). LIVE (fixed dist, same boot command, fresh DB): ERROR 0, TEMPLATE_NOT_FOUND 0, 'Failed to run background task' 0, 'Server is ready' 1, 'Dev admin ... seeded on empty DB' present; DB 'sys_email rows: 1' status sent, subject 'Verify your ObjectStack email address', related_object sys_user, related_id = the seeded admin's id; admin email_verified 1. So the verification send is rendered and persisted (logged-only through the dev LogTransport) and the admin is also stamped verified. Both boot process groups were killed by recorded PGID and their ports confirmed free. FINAL HEAD 404db56 merges origin/main be076da (hono adapter, showcase report, ADR note; no plugin-email or dependency change), so the package suite was not re-run there (AGENTS.md section 10 scoped re-check). Gates were re-run there.",
    "gates": "LOCAL at 404db56: 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' derived 64 commands (30 pnpm, 34 node), the same list as before the merge; all 64 were run, each exit captured before any pipe, all exit 0. 'dispatch-gates --ran': 'Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Includes check:engine-double-contract, check:nul-bytes, check:test-source-alias, check:cross-package-test-inputs, check:durability-log-level, check:plugin-teardown-shape, check:published-files, check:dual-build-cjs-loads and check:type-check-coverage. An earlier run of the 64 at pre-merge d879052 was also all 0: check:dual-build-cjs-loads first exited 3 PREREQUISITE NOT MET (8 packages had no dist; not counted as a measurement), the 8 were built (turbo cache), and the rerun gave exit 0. No platform-checklist run was needed: no exported symbol was renamed or deleted. CI check-runs on 404db56 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; the other 21 were in_progress (Lint · CI, release & engine gates; Lint · PM dispatch-gates; Lint · repo & tooling gates; Lint · ESLint; Lint · docs & code guards; Lint · scoped x3; Type Check · source gates / consumer gates / debt ledger / workspace; Check Changeset; Check PR Size; Governed Surface Queue Guard; The card this PR closes must claim this branch; Part-of PR must not also close its card; No other open PR may claim the same single-writer path; Flag docs affected by code changes; Check Documentation Links; filter). These are recorded as in_progress and were not waited on, per the contract.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 platform writes in 2 fleet-write relay strokes (each stroke is one POST /repos/objectstack-ai/objectstack/dispatches under the session token, executed as objectstack-fleet[bot]): (1) stroke fw-20261017T010500Z-6ab5c0, pr_create: POST /repos/objectstack-ai/objectstack/pulls (#24113, draft forced) + POST /repos//issues/24113/assignees (os-elon-musk), read back with the body byte-identical (9673 bytes); (2) this report: POST /repos//issues/23946/comments via scripts/pm/post-stamped.mjs. 0 label writes: the dispatch named no label and the diff ships a patch changeset, so skip-changeset does not apply. The 5 git pushes are not REST writes.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24113 · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-17T01:27Z. Landing through the merge queue.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 (#21118, stay-behind) · os-elon-musk · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-17T01:58Z.

    Landed: PR #24113 (Fixes #23946) merged through the merge queue as 33228c5b6c. That commit is on origin/main (git merge-base --is-ancestor). Timeline: merged 2026-10-17T01:57Z. ACCEPT 6227946488.

    Probe on origin/main Reading
    plugin-email/src/email-plugin.ts: init wires the engine before registerService('email') (if (engine) this.wireEngine(ctx, engine);) present, line 427

    What an operator sees: a verification email sent during boot, before kernel:ready, now finds its template and persists its sys_email row. Before, it threw TEMPLATE_NOT_FOUND. An engine registered only later is still wired at kernel:ready, once per engine.

    pm:dispatched is cleared and the assignee is released in this act.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions