Repository navigation
Commit 33228c5
fix(plugin-email): wire sys_email persistence + the template loader in init, so a boot-time verification send stops throwing TEMPLATE_NOT_FOUND (#24113)
Fixes #23946
Clause-②: no
## What was wrong
On a boot with email verification required, `objectstack dev
--seed-admin` (the default) logged this once, before the server was
ready:
```
ERROR [Better Auth]: Failed to run background task: Error: TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService
```
That verification send also left no `sys_email` row.
## Premise, checked on `origin/main` (`8a83cebc15`) before any change
- Reproduced once on a fresh DB, from `examples/app-showcase`:
`OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true`, `objectstack dev --ui
--seed-admin -p PORT -d file:FRESH/data.db`.
- The boot printed the ERROR line exactly once.
- Afterwards the DB had **0** `sys_email` rows.
- The seeded admin's row had `email_verified = 1`.
- The order, as the stack trace shows it:
1. `AuthPlugin.maybeSeedDevAdmin` calls `signUpEmail`.
2. better-auth calls `runInBackgroundOrAwait`, and with no background
handler configured it **awaits** `sendVerificationEmail`.
3. That reaches `EmailService.sendTemplate`, then
`resolveAndRenderTemplate`, which throws (`email-service.ts:1315`).
- Why the loader was missing at that point:
- The seed runs in plugin-auth's own `kernel:ready` hook.
- The served composition starts `Auth` ahead of `EmailServicePlugin`
(boot plugin list: `…, Auth, …, SettingsServicePlugin,
EmailServicePlugin, …`). The kernel runs `kernel:ready` handlers one at
a time, in the order the plugins started.
- `EmailServicePlugin` installed persistence and the loader
(`setPersistence` / `setTemplateLoader`) only inside **its own**
`kernel:ready` hook. So the seed's send reached a service that had
neither.
- The card says `packages/cli/src/commands/dev.ts` "signs the admin up
in-process". In fact `dev.ts` only forwards `OS_SEED_ADMIN`. The sign-up
runs in plugin-auth: `AuthPlugin.maybeSeedDevAdmin`, a `kernel:ready`
hook.
## The fix: option (a), wire persistence + the template loader in `init`
`EmailServicePlugin.init` now resolves the data engine (`objectql`, else
`data`) and installs `sys_email` persistence and the registry template
loader. It does this before it registers the `email` service. The wiring
itself moved unchanged into a `wireEngine` method that runs once per
engine. The `kernel:ready` hook calls the same method, which wires only
a host whose engine registered after `init`, so `init` records nothing
when the engine is absent.
Everything the two read is available at `init`. Each point was checked
in the code, not assumed:
- `objectql` is registered in `ObjectQLPlugin.init`
(`packages/objectql/src/plugin.ts:645`). ObjectQL is a hard
`dependencies` entry of this plugin, so it inits first.
- `engine.registry` is a field set when the engine is constructed
(`packages/objectql/src/engine.ts:4448`).
- The loader resolves `protocol` on every load, not at wiring time.
- Persistence calls `engine.insert` / `engine.update` only when a
message is sent. The live boot below shows that in the seed's window the
row lands.
**Why not (b), holding the send until the plugin's own `kernel:ready`.**
The early send in this boot is awaited inside AuthPlugin's
`kernel:ready` hook, by better-auth and then by the seed. The kernel
runs this plugin's `kernel:ready` handler only after that hook returns.
A hold that waits would therefore block until its bounded wait ran out,
on every verification-armed dev boot, and then still not send. A hold
that does not wait would answer `queued` with no row behind it. A crash
would lose that message, and the resend path would lose its error.
Option (a) needs neither, because nothing the loader reads arrives later
than `init`.
**The card's other option (the dev seed not firing `sendOnSignUp`) is
now moot.** Measured below: the send is accounted for, and the ERROR is
gone. As noted above, that option would land in plugin-auth, not in
`packages/cli`.
The log level is not lowered and no error is swallowed. A missing
template still throws `TEMPLATE_NOT_FOUND: NAME (locale=…)`, as before.
## Pin and ablation
The pin is in `email-plugin.outbox-sweep.test.ts`. It reuses that file's
existing engine double, so the engine-double ledger does not change. It
boots a real `LiteKernel` (the same `resolvePluginOrder` as
`ObjectKernel`) with three plugins, registered in this order:
1. ObjectQL's slot: the engine, the manifest door, and a `protocol` that
serves the templates the manifest door was handed.
2. A stand-in for plugin-auth's seed. Like `AuthPlugin`, it depends only
on ObjectQL. From its own `kernel:ready` it sends `auth.verify_email`
with the same arguments plugin-auth uses.
3. `EmailServicePlugin`.
What the pin asserts:
- **Positive control.** When the send runs, the email plugin's outbox
drain hook is not installed yet (0). After boot it is (1).
- No error is thrown.
- The send answers `status: sent`.
- Exactly one `sys_email` row exists, at `sent`, with the rendered
subject `Verify your Acme email address`, `related_object: sys_user` and
the expected `related_id`.
- The transport was called exactly once, so the boot sweep that runs
afterwards does not send it again.
Ablation used `node scripts/ablation-replace.mjs` to delete the `init`
call `if (engine) this.wireEngine(ctx, engine);`. The fix was committed
first, so HEAD holds it.
- The anchor went from 1 to 0 and the marker from 0 to 1. Blob
`8e8ca0e6d6a6` became `855acf5ca71b`.
- The pin went **red** with `expected Error: TEMPLATE_NOT_FOUND: no
templateLoader configured on EmailService to be undefined`. The other 13
tests in the file stayed green.
- Restore: blob is back to `8e8ca0e6d6a6`, equal to HEAD, `git diff
HEAD` is empty, and `git status` is clean.
- The pin imports `./email-plugin.js` relatively, so vitest reads `src/`
and no `dist/` rebuild leg applied.
## Live reading: one verification-armed boot on the fixed build
This used the same command on a fresh DB, after `pnpm --filter
@objectstack/plugin-email build`.
- The boot log has 0 `ERROR` lines, 0 `TEMPLATE_NOT_FOUND`, 0 `Failed to
run background task`, and 1 `Server is ready`. The `Dev admin … seeded
on empty DB` banner is present.
- The DB has 1 `sys_email` row: `status: sent`, subject `Verify your
ObjectStack email address`, `related_object: sys_user`, and `related_id`
equal to the seeded admin's `sys_user.id`.
- The seeded admin is also `email_verified = 1`.
- So the send happens and is persisted. It went through the boot's
`LogTransport`, because no mail transport is configured in dev, so it
was logged rather than delivered. The seed then stamps the admin
verified.
## Tests
- `pnpm --filter @objectstack/plugin-email test`: 34 files, 552 tests
passed.
- `pnpm --filter @objectstack/plugin-email typecheck`: `tsc --noEmit`
passed. `check:test-typecheck` reported OK with 0 errors. `tsc -p
tsconfig.test.json --listFiles` includes the edited test file.
## Gates (local subset, at final head `404db56cca`)
The branch merges `origin/main` at `be076dad28`. The incoming commits
touch the hono adapter, a showcase report and an ADR note, none of which
overlap plugin-email. After the merge I rebuilt `@objectstack/hono`.
- I derived the commands with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` at `404db56cca`. That gave
64 commands (30 `pnpm`, 34 direct `node`), the same list as before the
merge.
- All 64 were run, each exit code captured before any pipe, and every
one exited 0.
- Reconciliation with `--ran`: 64 derived, 64 run, 0 NOT-MEASURED, 0
UNRUN.
- Among the 64: `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 same 64, at the pre-merge head `d879052e5b`, was
also all 0. In that run `check:dual-build-cjs-loads` first exited 3 with
`PREREQUISITE NOT MET`, because eight packages had no `dist/`. It went
green once they were built.
- I did not run the CI-owned whole-repo scans or `test:repo` locally.
They belong to this PR's CI check-runs.
## Acceptance notes
- **A send in this window uses the transport and sender the boot was
configured with.** The `mail` settings namespace (Settings, then Mail)
is still applied at this plugin's own `kernel:ready`, which comes after.
On a deployment whose only SMTP configuration lives on the settings
page, such a send would go through the constructor transport. Two
qualifications:
- A plain `send()` in the same window already behaved this way before
this change, with no row at all.
- The only sender measured in the window is plugin-auth's dev seed,
which is development-only and stamps the admin verified right after.
- No production sender in the window was measured, so nothing was filed.
Carrier: none.
- **A deployment that declares queue delivery
(`OS_EMAIL_QUEUE_ENABLED`)** still delivers a send in this window
inline, because the delivery mode is applied at `kernel:ready`. This is
unchanged, except that the send now leaves its row.
- **A send during another plugin's `init` / `start`, before ObjectQL
connects its drivers,** would now attempt the `sys_email` insert. That
insert fails non-fatally (the existing `warn`) and the mail still goes
out inline. No such sender was measured.
- **Out of scope for this PR:** the card's option 1, the dev seed not
firing `sendOnSignUp`. It is unchanged, and moot as described above.
- Labels: the dispatch named none and the diff ships a changeset, so no
label was written.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c40a0dd commit 33228c5
3 files changed
Lines changed: 231 additions & 61 deletions
File tree
- .changeset
- packages/plugins/plugin-email/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
18 | 20 | | |
19 | 21 | | |
20 | 22 | | |
| |||
493 | 495 | | |
494 | 496 | | |
495 | 497 | | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
252 | 264 | | |
253 | 265 | | |
254 | 266 | | |
255 | 267 | | |
256 | 268 | | |
257 | 269 | | |
258 | 270 | | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
263 | 276 | | |
264 | 277 | | |
265 | 278 | | |
| |||
301 | 314 | | |
302 | 315 | | |
303 | 316 | | |
| 317 | + | |
| 318 | + | |
304 | 319 | | |
305 | 320 | | |
306 | 321 | | |
| |||
383 | 398 | | |
384 | 399 | | |
385 | 400 | | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | 401 | | |
390 | 402 | | |
391 | 403 | | |
392 | 404 | | |
393 | 405 | | |
394 | 406 | | |
395 | 407 | | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
396 | 429 | | |
397 | 430 | | |
398 | 431 | | |
399 | 432 | | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
400 | 497 | | |
401 | 498 | | |
402 | | - | |
403 | | - | |
404 | | - | |
| 499 | + | |
405 | 500 | | |
406 | 501 | | |
407 | 502 | | |
| |||
586 | 681 | | |
587 | 682 | | |
588 | 683 | | |
589 | | - | |
590 | | - | |
591 | | - | |
592 | | - | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
599 | | - | |
600 | | - | |
601 | | - | |
602 | | - | |
603 | | - | |
604 | | - | |
605 | | - | |
606 | | - | |
607 | | - | |
608 | | - | |
609 | | - | |
610 | | - | |
611 | | - | |
612 | | - | |
613 | | - | |
614 | | - | |
615 | | - | |
616 | | - | |
617 | | - | |
618 | | - | |
619 | | - | |
620 | | - | |
621 | | - | |
622 | | - | |
623 | | - | |
624 | | - | |
625 | | - | |
626 | | - | |
627 | | - | |
628 | | - | |
629 | | - | |
630 | | - | |
631 | | - | |
632 | | - | |
633 | | - | |
634 | | - | |
635 | | - | |
636 | | - | |
637 | | - | |
638 | | - | |
639 | | - | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
640 | 689 | | |
641 | 690 | | |
642 | 691 | | |
| |||
0 commit comments