Skip to content

fix(email): short-circuit to no-op sender when EMAIL_ENABLED=false (closes #332)#333

Merged
cristim merged 6 commits into
feat/multicloud-web-frontendfrom
feat/email-nop-sender
May 11, 2026
Merged

fix(email): short-circuit to no-op sender when EMAIL_ENABLED=false (closes #332)#333
cristim merged 6 commits into
feat/multicloud-web-frontendfrom
feat/email-nop-sender

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 11, 2026

Summary

Two related fixes to internal/email/ so the package matches its
documented contract and produces useful diagnostics when degraded.

Changes

1. Factory short-circuits to no-op sender on EMAIL_ENABLED=false (closes #332)

internal/email/factory.go::NewSenderFromEnvironment now returns a
no-op SenderInterface at the top of the function when
EMAIL_ENABLED=false, before dispatching by SECRET_PROVIDER.

The factory and the secret resolver share SECRET_PROVIDER for two
different purposes (cloud email backend vs. secret-store backend).
The local-dev docker-compose.yml already declares
EMAIL_ENABLED: "false" and explains "Email disabled for local
development (no SNS topic)", but the factory never reads the flag.
So SECRET_PROVIDER=env (the documented local-dev resolver per
internal/secrets/resolver.go:50) + EMAIL_ENABLED=false hard-fails
on startup with failed to initialize email sender: unsupported email provider: env, even though no email will ever be sent.

The new internal/email/nop_sender.go implements all 15
SenderInterface methods, logs each invocation at debug level so
local-dev traces still show where an email would have gone, and is
guarded by a compile-time
var _ SenderInterface = (*NopSender)(nil) assertion.

2. Log HTML approval-request render fallback

sendPurchaseApprovalRequestVia in internal/email/templates.go
silently swallowed RenderPurchaseApprovalRequestEmailHTML errors
when degrading to text-only delivery. Now emits
logging.Warnf("email: HTML approval-request render failed, falling back to text-only: %v", htmlErr) at the fallback site so
template regressions surface in logs without breaking email delivery.
The graceful fallback itself is unchanged — htmlBody = "" still
routes through the multipart sender's text-only path.

Originally a CodeRabbit nitpick on PR #298 that raced with the merge
(commit eb09eabe4 on the closed feat/issue-287 branch). Folded
into this PR since both changes touch internal/email/ and the
change is six lines.

Closes #332.

Test plan

  • go build ./... clean
  • go test ./... clean (0 failures)
  • go test ./internal/email/... clean
  • Manual: start the local docker-compose stack with
    SECRET_PROVIDER=env and EMAIL_ENABLED=false; backend reaches
    /api/health HTTP 200; logs show
    "Email sending is disabled (EMAIL_ENABLED=false); using no-op sender".

Summary by CodeRabbit

  • New Features

    • Option to fully disable email delivery via an environment setting; when disabled the system suppresses outbound emails and logs the change.
  • Improvements

    • Added a no-op email sender that silently suppresses sends while logging minimal, non-PII details.
    • Approval email template render failures now log a warning and fall back to plain-text delivery.
  • Tests

    • Added tests verifying the disable flag behavior and the no-op sender's nil-safe, no-error contract.

Review Change Stack

cristim added 2 commits May 11, 2026 21:28
…loses #332)

`internal/email/factory.go::NewSenderFromEnvironment` now returns a no-op
`SenderInterface` at the top of the function when `EMAIL_ENABLED=false`,
before dispatching by `SECRET_PROVIDER`.

Background: the factory and the secret resolver share `SECRET_PROVIDER`
for two different purposes (cloud email backend selection vs.
secret-store backend selection). The local-dev `docker-compose.yml`
already declares `EMAIL_ENABLED: "false"` and explains "Email disabled
for local development (no SNS topic)", but the factory never reads the
flag — it just dispatches on `SECRET_PROVIDER`. So:

  - `SECRET_PROVIDER=aws` + no AWS creds boots an SES sender that
    nothing exercises (passes by luck, not design).
  - `SECRET_PROVIDER=env` (the documented local-dev resolver per
    `internal/secrets/resolver.go:50`) + `EMAIL_ENABLED=false` hard-fails
    on startup: `failed to initialize email sender: unsupported email
    provider: env`.

The factory now respects the documented `EMAIL_ENABLED` contract. The
no-op sender implements all 15 `SenderInterface` methods, logs each
invocation at debug level so local-dev traces still show where an email
would have gone, and is guarded by a compile-time
`var _ SenderInterface = (*NopSender)(nil)` assertion.

Verification:
  - `go build ./...` clean
  - `go test ./...` clean (0 failures)
…diagnosis

`sendPurchaseApprovalRequestVia` in `internal/email/templates.go`
silently swallowed `RenderPurchaseApprovalRequestEmailHTML` errors
when degrading to text-only delivery. The graceful fallback is correct
(text-only is the safer alternative to dropping the approval email
entirely), but the silent swallow hides template-syntax bugs from
production diagnostics.

This commit imports the project's standard `pkg/logging` package and
emits a `Warnf` at the fallback site so an HTML render regression
surfaces in logs without breaking email delivery. The fallback itself
is unchanged — `htmlBody = ""` still routes through the multipart
sender's text-only path. A comment notes the deliberate non-return
decision so a future reader doesn't escalate the warning into an
early `return err`.

Originally a CodeRabbit nitpick on PR #298 that raced with the merge
(commit eb09eab on the closed feat/issue-287 branch). Folded into
this PR per follow-up tracking — both touch internal/email/ and the
change is six lines.

Verification:
  - `go build ./...` clean
  - `go test ./internal/email/...` clean
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/xs Trivial / one-liner type/bug Defect labels May 11, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eadc1df7-e611-4a6d-852b-a23f5d0cd61b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a no-op email sender and integrates an EMAIL_ENABLED early-exit in the factory to return it when disabled; it also logs HTML template render failures for purchase-approval emails while keeping text-only fallback.

Changes

Email Disabling with No-op Sender

Layer / File(s) Summary
No-op Sender Implementation
internal/email/nop_sender.go
Defines NopSender with NewNopSender() and implements all SenderInterface methods. Each method suppresses delivery by returning nil and emits debug logs with only limited non-PII metadata. Includes compile-time interface satisfaction check.
Factory Early-Exit Logic
internal/email/factory.go
Adds strconv import and updates NewSenderFromEnvironment to read EMAIL_ENABLED; when set and parsed to false it logs and returns NewNopSender() early. Unparseable values emit a warning and continue normal provider detection.
Template Error Logging
internal/email/templates.go
Adds pkg/logging import and changes sendPurchaseApprovalRequestVia to log a logging.Warnf if HTML template rendering fails; preserves non-fatal text-only fallback (HTML body set to "").
Tests
internal/email/factory_test.go, internal/email/nop_sender_test.go
Adds tests verifying EMAIL_ENABLED gating behavior in the factory and that NewNopSender() methods all return nil and are nil-safe; also runtime interface assignment check.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#298: Related prior changes to the email sender interface and HTML render fallback handling.

Suggested labels

severity/low, urgency/this-quarter, effort/s

Poem

🐰 A silent sender hops in place,
Debugging traces, no outbound race.
EMAIL_ENABLED whispers "no",
Factory returns the quiet show.
Templates warn when HTML fails—soft grace.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and specifically summarizes the main change: adding a short-circuit to a no-op sender when EMAIL_ENABLED=false, with reference to the closed issue.
Linked Issues check ✅ Passed All objectives from issue #332 are met: EMAIL_ENABLED=false triggers no-op sender (factory.go), all 15 SenderInterface methods implemented with compile-time assertion (nop_sender.go), debug-level logging implemented, HTML render fallback logs warning (templates.go), and tests verify all requirements.
Out of Scope Changes check ✅ Passed All changes are within scope: factory.go reads EMAIL_ENABLED and returns NopSender, nop_sender.go implements the no-op type, templates.go adds logging as a bundled follow-up per #332, and test files validate both.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/email-nop-sender

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/email/factory.go`:
- Around line 47-50: Replace the literal string check of EMAIL_ENABLED with
boolean parsing: read os.Getenv("EMAIL_ENABLED"), call strconv.ParseBool on it
(import strconv), and if the parsed value is false return NewNopSender() and log
the same message; handle parse errors by either treating them as enabled (and
optionally logging a warning) or defaulting to false per your policy. Ensure you
reference the same env var name "EMAIL_ENABLED" and keep NewNopSender() as the
no-op return path.

In `@internal/email/nop_sender.go`:
- Around line 26-33: NopSender currently logs raw recipient and CC email
addresses in SendToEmail and SendToEmailWithCCMultipart (and other no-op sender
methods), which leaks PII; update those logging calls to avoid printing raw
emails by either removing the address fields or replacing them with non-PII info
(e.g., masked addresses, recipient count, or domains only); implement a small
helper (e.g., maskEmail or summarizeRecipients) and use it in
NopSender.SendToEmail and NopSender.SendToEmailWithCCMultipart (and the other
no-op methods flagged) so logs retain useful context without exposing full email
addresses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d427b08-aaa4-448c-9218-950a35d64802

📥 Commits

Reviewing files that changed from the base of the PR and between c95a6ac and a553b96.

📒 Files selected for processing (3)
  • internal/email/factory.go
  • internal/email/nop_sender.go
  • internal/email/templates.go

Comment thread internal/email/factory.go Outdated
Comment thread internal/email/nop_sender.go Outdated
… no-op logs (CR pass on PR #333)

Addresses both actionable items from CodeRabbit's review on PR #333:

1. **EMAIL_ENABLED parsing** (`internal/email/factory.go`): the
   literal `== "false"` check only matched the lower-case spelling.
   `"False"`, `"FALSE"`, `"0"`, etc. would all be treated as enabled,
   even though the user clearly intended the no-op path. Replaced
   with `strconv.ParseBool` so the full set of false-y spellings is
   honoured (false/False/FALSE/0/f/F per stdlib semantics). Unset /
   empty values keep the default (enabled) so existing deployments
   that don't set the var are unaffected. Unparseable values emit a
   warning and fall through to the enabled path so the
   misconfiguration is visible in logs without bringing the app
   down.

2. **PII redaction** (`internal/email/nop_sender.go`): the previous
   logs included raw recipient email addresses, subjects, and (in
   one place) admin email addresses. Even in dev, log files leak
   into shared environments (terminal scrollback, screen-shares,
   support tickets), and email addresses are sufficient identifying
   information to require treating them as PII. Replaced all
   recipient/subject/email-data formatting with just method-name +
   recipient counts (e.g. `to=1 cc=3`) — enough trace context to
   know which email path was invoked without leaking who it would
   have gone to. Documented the policy in the type-level doc
   comment so a future maintainer doesn't add the addresses back.

Verification:
  - `go build ./...` clean
  - `go test ./...` clean (0 failures)
  - `go test ./internal/email/...` clean
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

@coderabbitai — addressed both actionable items from review 4266799238 in commit 46c0609b7:

  1. EMAIL_ENABLED parsing — switched to strconv.ParseBool so the full false-y spelling set is honoured (false/False/FALSE/0/f/F). Unset/empty keeps the default (enabled) so existing deployments are unaffected; unparseable values emit a Warnf and fall through to enabled, so a misconfiguration is visible in logs without bringing the app down.

  2. PII redaction in no-op logs — removed recipient emails, subjects, and embedded admin emails from every NopSender.* log line. Replaced with method-name + recipient counts (to=1 cc=3) — enough trace context to know which path was invoked without leaking who it would have gone to. Documented the policy in the NopSender type doc comment so a future maintainer doesn't add addresses back. Considered using the existing redactEmail helper in internal/auth/service_password.go but it's package-private and even a masked address is gratuitous for a no-op sender.

Verification: go build ./... clean, go test ./... clean (0 failures), go test ./internal/email/... clean.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

@cristim — thanks for the thorough follow-up. Both changes look well-considered:

  1. strconv.ParseBool for EMAIL_ENABLED — idiomatic Go, covers the full boolean spelling set, and the warn-then-fall-through behaviour for unparseable values is the right trade-off (visible misconfiguration without a hard crash). The "unset = enabled" default preserves backward compatibility for existing deployments.

  2. PII redaction in no-op logs — replacing email addresses and subjects with to=N cc=N counts is exactly the right level of detail for a dev/trace path: enough to confirm which code branch fired, nothing that leaks PII into log sinks. Documenting the policy in the NopSender type comment is a nice touch to guard against future regressions.

Triggering a fresh review of the updated commit now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Tests added alongside the runtime changes from this PR.

`internal/email/factory_test.go` gains
`TestNewSenderFromEnvironment_EmailEnabled` — a table-driven test with
fifteen sub-cases covering every branch of the new dispatch:

  - Six false-y EMAIL_ENABLED spellings (false, False, FALSE, 0, f, F)
    all short-circuit to NopSender, with SECRET_PROVIDER set to a
    deliberately-unsupported "env" backend so the assertion proves
    the short-circuit fires BEFORE provider dispatch.
  - Six true-y spellings (true, True, TRUE, 1, t, T) all fall through
    to the AWS sender via SECRET_PROVIDER=aws.
  - Unset / empty values fall through to the default-enabled path.
  - An unparseable value ("maybe") emits a warning (visible in test
    output) and falls through to enabled — protecting an accidental
    misconfiguration from taking the app down.

`internal/email/nop_sender_test.go` (new file) covers the NopSender
contract:

  - TestNopSender_AllMethodsReturnNil — exercises every one of the 15
    SenderInterface methods and asserts they all return nil. A future
    bug that returns an error would propagate into the calling
    application path, breaking the EMAIL_ENABLED=false promise that
    no work happens.
  - TestNopSender_NilSafe — empty / nil inputs (empty CC list, empty
    bodies) must not panic.
  - TestNopSender_NilContext — a nil context.Context must not panic
    either; the no-op shouldn't be the surface that leaks a panic.
  - TestNopSender_SatisfiesInterface — runtime echo of the
    compile-time `var _ SenderInterface = (*NopSender)(nil)` guard,
    so a refactor that silently removes the guard is still caught.

Verification:
  - `go test ./internal/email/...` clean (19 new sub-cases)
  - `go test ./...` clean (0 failures)
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

Added unit tests in commit b04594e:

  • TestNewSenderFromEnvironment_EmailEnabled — 15 sub-cases covering every branch (6 false-y spellings short-circuit to NopSender with SECRET_PROVIDER=env to prove the short-circuit fires before dispatch; 6 true-y spellings fall through to AWS sender; unset/empty fall through; unparseable warns + falls through).
  • TestNopSender_AllMethodsReturnNil — exercises every one of the 15 SenderInterface methods on NopSender.
  • TestNopSender_NilSafe — nil/empty inputs don't panic.
  • TestNopSender_NilContext — nil context doesn't panic.
  • TestNopSender_SatisfiesInterface — runtime echo of the compile-time guard.

go test ./... clean. @coderabbitai review

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

@cristim — the test suite looks thorough and well-structured:

  • TestNewSenderFromEnvironment_EmailEnabled with 15 sub-cases is exactly the right approach — pinning every false-y/true-y spelling plus the unset/empty/unparseable edge cases to concrete assertions.
  • TestNopSender_NilSafe and TestNopSender_NilContext are good defensive additions; nil-input panics in no-op paths are a common footgun that often gets missed.
  • TestNopSender_SatisfiesInterface as a runtime echo of the compile-time guard is a nice belt-and-suspenders touch.

Triggering a fresh review of the updated commit now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/email/factory_test.go`:
- Around line 258-260: In the "unset_falls_through" subtest, capture the
original EMAIL_ENABLED value before calling os.Unsetenv and register a t.Cleanup
that restores the environment after the test; i.e., inside the
t.Run("unset_falls_through", ...) before os.Unsetenv("EMAIL_ENABLED") save orig
:= os.LookupEnv("EMAIL_ENABLED") and call t.Cleanup(func(){ if present restore
with os.Setenv else call os.Unsetenv }) so the test (and helper aws(t)) does not
leak mutated env state to later tests.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 416d50af-d1a3-49e1-8793-42bc7d814ab1

📥 Commits

Reviewing files that changed from the base of the PR and between 46c0609 and b04594e.

📒 Files selected for processing (2)
  • internal/email/factory_test.go
  • internal/email/nop_sender_test.go

Comment thread internal/email/factory_test.go
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • internal/email/factory_test.go

Commit: b415e7e5485be882742094bc944dbe1a0920a105

The changes have been pushed to the feat/email-nop-sender branch.

Time taken: 2m 17s

coderabbitai Bot and others added 2 commits May 11, 2026 20:04
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@cristim cristim merged commit 80ef684 into feat/multicloud-web-frontend May 11, 2026
4 of 5 checks passed
cristim added a commit that referenced this pull request May 11, 2026
PR #333 (closes #332) landed a botched merge of `unset_falls_through`
in TestNewSenderFromEnvironment_EmailEnabled — two overlapping copies
of the same sub-test ended up concatenated (one missing its closing
braces, one preceded by stray fragments), plus duplicate
`ctx :=` / `sender, err :=` lines. The result didn't parse:

  gofmt -l internal/email/factory_test.go
  internal/email/factory_test.go:309:2: missing ',' before newline ...
  internal/email/factory_test.go:312:6: expected '(', found TestNewSenderWithConfig_AWS

This trips `gofmt` and `go vet` in the pre-commit workflow on every
open PR against `feat/multicloud-web-frontend` (e.g. #326, #335, #336).

Keep the `prev/hadPrev` version of the sub-test (the one that actually
does `os.Unsetenv` first, which is the case the test name describes),
drop the orphaned `orig/hadOrig` fragment, and remove the duplicate
ctx/sender declarations.

Verified locally: gofmt clean, `go vet ./internal/email/...` clean,
`go test ./internal/email/...` 306 tests pass.
cristim added a commit that referenced this pull request May 11, 2026
…env vars (closes #334) (#335)

* fix(local-dev): docker-compose + .env.example cover the new required env vars (closes #334)

A fresh `docker-compose up -d` on this branch fails on startup because
the compose file is missing several env vars the app now requires.
This commit adds them with local-dev defaults and documents the same
contract in `.env.example` so anyone running outside docker-compose
(e.g. directly via Air or `go run ./cmd/server`) has a one-stop
reference.

Failure chain that this fixes (each gated the next):

  1. scheduled-task auth init: SCHEDULED_TASK_AUTH_MODE unset
  2. admin password resolution: ADMIN_PASSWORD_SECRET required
  3. (after #333 lands) SECRET_PROVIDER=aws + empty AWS creds was
     working by luck; switching to `env` is now the correct
     local-dev resolver path
  4. frontend admin-setup modal asks for an API key (sourced from
     API_KEY_SECRET_ARN → fails when the var is empty)

`docker-compose.yml` (`app` service environment block):

  - SECRET_PROVIDER: aws → env (internal/secrets.EnvResolver, per
    internal/secrets/resolver.go:50 — pairs with EMAIL_ENABLED=false
    so the no-op email sender from #333 kicks in).
  - SCHEDULED_TASK_AUTH_MODE: disabled (internal/server/scheduledauth
    has no default and refuses to start when unset).
  - ADMIN_PASSWORD_SECRET / API_KEY_SECRET_ARN as VAR-NAME indirections
    pointing at ADMIN_PASSWORD_DEV / ADMIN_API_KEY_DEV (the EnvResolver
    pattern). Concrete dev values for both, plus ADMIN_EMAIL.
  - CREDENTIAL_ENCRYPTION_ALLOW_DEV_KEY=1 (gate the all-zero dev key
    per credentials.LoadKey — refuses to start without it).

`.env.example` documents:

  - the new SECRET_PROVIDER=env contract (replaces the now-stale
    "env will fail" warning that pre-dated #333),
  - SCHEDULED_TASK_AUTH_MODE and EMAIL_ENABLED with one-line rationale,
  - the VAR-NAME-indirection pattern for *_SECRET / *_SECRET_ARN with
    the concrete dev values co-located so future readers can trace
    the chain in one file.

Depends on PR #333 (no-op email sender) — without that, the email
factory crashes on SECRET_PROVIDER=env. Sequencing intentional.

Verification:
  - docker-compose up -d brings postgres + app + frontend to healthy
  - curl http://localhost:8080/api/health → HTTP 200
  - admin-setup modal accepts the documented dev defaults

* fix(local-dev): align .env.example ADMIN_EMAIL with docker-compose default (CR pass on PR #335)

CodeRabbit nitpick on PR #335: `.env.example` still listed
`ADMIN_EMAIL=admin@example.com` while `docker-compose.yml` defaults to
`admin@cudly.local`. The drift made the two reference points disagree
about which placeholder a fresh checkout should use. Aligning on
`admin@cudly.local` keeps both files telling the same story.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant