Stalwart Email
Notes
Board 1
-
Stalwart Email Board (RETIRED)
board-stalwart-emailNo content
Project Page 1
-
Stalwart Email (RETIRED — replaced by postmark-email)
project-stalwart-emailVision
Self-hosted SMTP infrastructure for the Pal-E platform. Stalwart Mail Server on the Hetzner edge VPS enables custom-domain transactional email (password resets, notifications) without third-party dependencies. Each service realm gets its own sender identity (noreply@landscaping-assistant.app, noreply@westsidekingsandqueens.com). Infrastructure lives in pal-e-platform (Salt state), per-domain configuration in pal-e-services (DNS + Keycloak SMTP). Zero Gmail dependency for auth flows.
User Stories
Key Role Story Success Metric password-reset End User As a user who forgot my password, I want to receive a reset email from my app's domain so I can regain access Keycloak "Forgot Password" sends email from noreply@{domain}, lands in inbox (not spam), link works custom-domain Platform Admin As platform admin, I want each app to send from its own domain so emails look professional and build domain reputation Each realm configured with distinct sender. SPF/DKIM/DMARC all pass. mail-tester.com score 9+/10 self-hosted Platform Admin As platform admin, I want zero third-party email dependencies so I control deliverability and avoid Gmail app password rotation Stalwart handles all outbound SMTP. No Gmail OAuth tokens needed for auth flows. Architecture
Browser (user clicks "Forgot Password") | v Keycloak (k3s, landscaping realm) | SMTP AUTH via Tailscale (100.110.151.59:587) v Stalwart Mail Server (Hetzner edge VPS, 178.156.129.142) | STARTTLS + DKIM signing v Gmail/Outlook/Yahoo MX servers | v User's inbox: "From: noreply@landscaping-assistant.app"Key Decisions
- Stalwart on edge VPS (not k3s) — mail server must live on the IP with PTR/rDNS for deliverability. Home ISPs block port 25 and don't support rDNS.
- SaltStack managed — single binary + systemd, same as Caddy. Fits existing edge VPS management pattern.
- Outbound-only — no inbound MX. Only port 587 (submission from Keycloak via Tailscale) and port 25 outbound (delivery to remote MX).
- SMTP AUTH per realm — each Keycloak realm authenticates as its own noreply@ account. Auditable, defense-in-depth.
- Caddy for web admin only — mail.landscaping-assistant.app reverse proxies to Stalwart's HTTP admin UI.
Board
board-stalwart-emailStatus
Not started. Research complete. Tickets scoped. First blocker: Hetzner port 25 unblock request.
Milestones
None yet.
Repos
Repo Platform Role pal-e-platform Forgejo Salt state for Stalwart install, Hetzner firewall rules, PTR record pal-e-services Forgejo DNS records per domain (SPF/DKIM/DMARC), Keycloak SMTP config per realm Related
project-pal-e-mail— application-level email sending service (Gmail-based). Future: could migrate to use Stalwart as backend.
Review 14
-
Review: Stalwart: domain + account setup for landscaping-assistant.app
review-1736-2026-07-04-v2Verdict: READY
Re-review of board item #1736. Previous review (review-1736-2026-07-04) returned NEEDS_REFINEMENT. All three issues have been addressed.
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- "Depends on: pal-e-platform#498 (Stalwart install)"
- [x] Repo -- ldraney/pal-e-services (with cross-repo note explaining pillar lives in pal-e-platform)
- [x] User Story -- "As a platform admin I want SMTP sending accounts configured..."
- [x] Context -- good detail on both domains and SMTP-only accounts
- [x] File Targets -- salt/pillar/stalwart.sls (correct, matches arch note)
- [x] Feature Flag -- "none"
- [x] Acceptance Criteria -- 6 criteria (verifiable)
- [x] Test Expectations -- swaks command with proper flags
- [x] Constraints -- present (strong passwords, send-only, RSA 2048-bit)
- [x] Checklist -- present
- [x] Related -- present
Traceability
- [x] story:self-hosted label -- "As platform admin, I want zero third-party email dependencies so I control deliverability"
- [x] story note verified -- found in project-stalwart-email user-stories section
- [x] arch:stalwart label -- references Stalwart Mail Server component
- [x] arch note verified -- arch-stalwart note exists with full architecture diagram and key files listing
- [x] Forgejo issue -- https://forgejo.tail5b443a.ts.net/ldraney/pal-e-services/issues/164, open
File Targets
- [x]
salt/pillar/stalwart.sls-- verified: directory salt/pillar/ exists in pal-e-platform with existing .sls files (caddy.sls, k3s.sls, firewall.sls, etc.). File does not exist yet (to be created). Path matches arch-stalwart note's Key Files section. Cross-repo note explicitly documents that this file lives in pal-e-platform. - [x] Credentials storage -- "Document SMTP account credentials in Salt GPG pillar or secrets pipeline" -- salt/pillar/secrets/ directory exists with existing encrypted pillar files (services.sls, platform.sls, forgejo.sls).
Repo Placement
Acceptable. Issue filed on ldraney/pal-e-services for project tracking. Explicit cross-repo note added: "Issue filed on pal-e-services for project tracking; Salt pillar configuration is in pal-e-platform (salt/pillar/stalwart.sls)." Board is project-level (board-stalwart-email), not repo-level, so the organizational choice is reasonable.
Dependencies
- Blocked by #1733 (pal-e-platform#498): "Salt: Stalwart install + systemd service state" -- currently in next_up column. Must complete before domains can be created. Documented in Lineage section.
- Blocks #1737 (pal-e-services#165): DNS SPF/DKIM/DMARC for landscaping-assistant.app -- in todo. Needs DKIM public key output from this ticket.
- Blocks #1738 (pal-e-services#166): DNS SPF/DKIM/DMARC for westsidekingsandqueens.com -- in todo. Needs DKIM public key output from this ticket.
- Downstream dependency documented in AC: "DKIM public keys extracted and documented in a note or pillar output (downstream DNS tickets #165/#166 depend on this)"
Acceptance Criteria
6 criteria, all verifiable:
- Domain existence (x2): verifiable via Stalwart admin API GET /api/domain/{name}
- SMTP AUTH (x2): verifiable via swaks command (test command provided in Test Expectations)
- DKIM key generation (x1): verifiable by checking keys exist after domain creation
- DKIM export (x1): verifiable by checking output artifact exists for downstream consumers
Test command is specific and real:
swaks --to test@gmail.com --from noreply@landscaping-assistant.app --server 127.0.0.1:587 --auth --auth-user noreply@landscaping-assistant.appBlast Radius
Low. Configuring a new service (Stalwart) with no modifications to existing services. Downstream consumers (DNS tickets, future Keycloak config) are separate tickets. No sibling services share this pattern since Stalwart is the only mail server in the platform.
Decomposition Assessment
1 file target in 1 repo. 6 acceptance criteria (slightly over 5 threshold), but criteria are repetitive -- same 3 operations (create domain, create account, export DKIM) applied to 2 domains. Actual complexity is low. Estimated agent time less than 5 minutes once Stalwart is running. No decomposition needed.
Previous Review Issues -- Resolution
- [x] File targets corrected: removed incorrect terraform/keycloak.tf reference, replaced with salt/pillar/stalwart.sls
- [x] Cross-repo note added: explicit documentation that issue is filed on pal-e-services but file changes target pal-e-platform
- [x] DKIM output artifact AC added: "DKIM public keys extracted and documented in a note or pillar output (downstream DNS tickets #165/#166 depend on this)"
Recommendation
No action needed. Ticket is ready for implementation once blocker #1733 (Stalwart install) completes.
-
Review: Caddy reverse-proxy block for mail.landscaping-assistant.app (v2)
review-1734-2026-07-04-v2Verdict: APPROVED
Re-review of board item #1734. Previous review (
review-1734-2026-07-04) returned NEEDS_REFINEMENT with three issues. All three have been resolved in the current issue body.Template Completeness
- [x] Type: Feature
- [x] Lineage
- [x] Repo
- [x] User Story
- [x] Context
- [x] File Targets
- [x] Feature Flag (none)
- [x] Acceptance Criteria
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related
All required sections present.
Traceability
- [x] story:self-hosted label — "As platform admin, I want zero third-party email dependencies so I control deliverability and avoid Gmail app password rotation"
- [x] story note verified — found in project-stalwart-email user-stories section (key: self-hosted, role: Platform Admin)
- [x] arch:edge-vps label — Hetzner Edge VPS component
- [x] arch note verified — arch-edge-vps note exists in pal-e-docs (title: "Architecture: Hetzner Edge VPS", project: stalwart-email)
- [x] Forgejo issue — ldraney/pal-e-platform#499, state: open
File Targets
- [x]
salt/pillar/caddy.sls— verified: file exists at expected path, containscaddy:sitespillar structure with domain/proxy_target/www_redirect keys for 3 existing sites - [x]
salt/states/caddy/Caddyfile.j2— verified: file exists, confirms hardcoded:443suffix andtls_server_nametransport for ALL upstreams. Ticket correctly identifies need forlocal: trueconditional to skip these for localhost targets
Previous issue resolved: Caddyfile.j2 now listed as file target with clear description of required template changes.
Repo Placement
Correct. Issue filed on ldraney/pal-e-platform, which contains both
salt/pillar/caddy.slsandsalt/states/caddy/Caddyfile.j2. Single-repo fix.Dependencies
- Blocking: Issue #498 (Stalwart install) — board item #1733, currently in next_up. Stalwart must be installed and running on port 8080 before the reverse proxy can route to it.
- Dependency is documented in the issue Lineage section: "Depends on: Stalwart install issue (same repo)."
Acceptance Criteria
4 AC + 1 test expectation. All verifiable by an agent post-implementation:
- "https://mail.landscaping-assistant.app loads Stalwart web admin" — curl + status code check
- "TLS auto-provisioned by Caddy (Let's Encrypt)" — cert issuer check via openssl s_client
- "Admin protected by Stalwart's built-in auth (no public registration)" — verify unauthenticated access returns 401/redirect
- "Existing sites (palinks.app, landscaping-assistant.app, westsidekingsandqueens.com) continue to resolve and serve HTTPS after Caddyfile.j2 template change" — regression curl checks on all 3 domains
- Test:
curl -I https://mail.landscaping-assistant.appreturns 200/302
Previous issue resolved: Regression criterion for existing sites now present.
Blast Radius
The Caddyfile.j2 template renders ALL sites (currently 3: palinks.app, landscaping-assistant.app, westsidekingsandqueens.com). The
local: trueconditional approach usessite.get('local', false)pattern, which defaults to false for existing sites — preserving their current behavior. Regression AC explicitly covers verification of existing sites post-change.Decomposition Assessment
- 2 file targets in 1 repo — acceptable
- 4 acceptance criteria + 1 test — under threshold
- Estimated agent work: under 5 minutes (1 pillar entry + 1 Jinja conditional)
- No decomposition needed
Context Verification
Previous issue resolved: The Context section now specifies the exact pillar schema with
local: trueflag:mail: domain: mail.landscaping-assistant.app proxy_target: localhost:8080 local: trueThis gives the implementing agent unambiguous guidance on the pillar structure extension.
Recommendation
No action needed. All previous NEEDS_REFINEMENT issues have been addressed. Ticket is ready for implementation.
-
Review: Stalwart: Create domains + SMTP accounts via API
review-1736-2026-07-04Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type — Feature
- [x] Lineage — "Depends on: pal-e-platform#498 (Stalwart install)"
- [x] Repo — present (but incorrect, see below)
- [x] User Story — present
- [x] Context — good detail on both domains and SMTP-only accounts
- [x] File Targets — present (but wrong targets, see below)
- [x] Feature Flag — "none"
- [x] Acceptance Criteria — 5 criteria
- [x] Test Expectations — swaks test command
- [x] Constraints — present
- [x] Checklist — present
- [x] Related — present
Traceability
- [x] story:self-hosted label — "As platform admin, I want zero third-party email dependencies so I control deliverability"
- [x] story note verified — found in project-stalwart-email user-stories section
- [x] arch:stalwart label — references Stalwart Mail Server component
- [x] arch note verified — arch-stalwart note exists in pal-e-docs with full architecture diagram
- [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/ldraney/pal-e-services/issues/164, open
File Targets
- [ ]
terraform/keycloak.tf— ISSUE: This file exists in pal-e-services and contains the dynamic smtp_server block (lines 47-60), but it is for DOWNSTREAM work (configuring Keycloak to use Stalwart). This ticket's scope is creating domains/accounts IN Stalwart, not configuring Keycloak to connect. - [ ] "Document SMTP account credentials in Salt GPG pillar or secrets pipeline" — ISSUE: Vague. Per arch-stalwart note, the actual target should be
salt/pillar/stalwart.slsin pal-e-platform (domains, accounts, DKIM keys). This file does not exist yet (created by #498).
Repo Placement
MISMATCH. Issue filed on
ldraney/pal-e-servicesbut:- Title says "via API" — implies Stalwart REST API calls (admin action on edge VPS, not code in pal-e-services)
- Per arch-stalwart: "SaltStack managed: Declarative state in salt/states/stalwart/; pillar-driven config" — Salt files live in
pal-e-platform - If automated via Salt pillar, repo should be
ldraney/pal-e-platform - If manual API calls, this is more of a Task type (no code files to modify)
- keycloak.tf in pal-e-services is the consumer of the credentials, not where they are created
Dependencies
- Blocked by #1733 (pal-e-platform#498): "Salt: Install Stalwart binary + systemd service on edge VPS" — currently in backlog, state OPEN. Stalwart must exist before domains can be created on it.
- Blocks #1737 (pal-e-services#165): DNS SPF/DKIM/DMARC for landscaping-assistant.app — needs DKIM public key output from this ticket
- Blocks #1738 (pal-e-services#166): DNS SPF/DKIM/DMARC for westsidekingsandqueens.com — needs DKIM public key output from this ticket
- Dependencies are partially documented (Lineage mentions #498; Related mentions "Blocked by: pal-e-platform#498"). Downstream blockers (#1737, #1738) are not documented in this issue but are captured on the board.
Acceptance Criteria
5 criteria — at the boundary but acceptable. All are verifiable:
- Domain existence: verifiable via Stalwart admin API GET
- Account auth: verifiable via swaks (test command provided)
- DKIM export: verifiable by checking key file exists
Missing criterion: "SMTP credentials stored in secrets pipeline / Salt pillar for downstream Keycloak config." This bridges to the downstream work.
Blast Radius
Low risk. This is a configuration action on a new service (Stalwart). No existing services are modified. The only downstream consumers are:
- DNS tickets (#1737, #1738) need the DKIM public keys as output
- Keycloak realm SMTP config (separate ticket needed) needs the account credentials
Decomposition Assessment
5 acceptance criteria, 1 repo (actual work is API calls or Salt pillar), estimated agent time <5 minutes once Stalwart is running. No decomposition needed.
Recommendations
[BODY]Fix File Targets — removeterraform/keycloak.tfreference (downstream Keycloak config, not this ticket's scope). Replace with: (a) If Salt-managed:salt/pillar/stalwart.slsin pal-e-platform — add domain/account entries; or (b) If API-driven: document that this is a manual/scripted Stalwart admin API action with credential output stored in secrets pipeline.[BODY]Fix Repo — if Salt-based automation, change toldraney/pal-e-platform. If manual API action, change Type to Task and replace File Targets with a Scope section describing the admin workflow.[BODY]Add output artifact — acceptance criteria should include "DKIM public keys written to a known location (e.g.,~/secrets/stalwart/dkim/) for downstream DNS tickets to consume."[LABEL]Consider addingstory:custom-domainas secondary label — this ticket directly enables per-domain sending (the custom-domain story), in addition to the self-hosted story.
-
Review: Salt: Stalwart install + systemd service state
review-1733-2026-07-04Verdict: READY
Scope is solid. All file targets verified against codebase. Traceability complete. Work follows established caddy state pattern exactly. Fits in a single agent pass.
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- Standalone, part of stalwart-email project
- [x] Repo -- ldraney/pal-e-platform
- [x] User Story -- As a platform admin, I want Stalwart installed via SaltStack
- [x] Context -- Describes Stalwart, edge VPS, why mail server needs this IP
- [x] File Targets -- 3 create + 1 modify, all paths valid
- [x] Feature Flag -- none (appropriate for infra)
- [x] Acceptance Criteria -- 6 items, all testable
- [x] Test Expectations -- 3 items with real commands
- [x] Constraints -- 4 items (unprivileged user, interface binding, pattern conformance)
- [x] Checklist -- Present
- [x] Related -- stalwart-email project referenced
Traceability
- [x] story:self-hosted label -- present on board item
- [x] story note verified -- "self-hosted" entry found in project-stalwart-email user-stories section ("As platform admin, I want zero third-party email dependencies")
- [x] arch:stalwart label -- present on board item
- [x] arch note verified -- arch-stalwart note exists (note_type: architecture, project: stalwart-email)
- [x] arch-edge-vps note also exists -- secondary component, not required as label since arch:stalwart is the primary
- [x] Forgejo issue -- ldraney/pal-e-platform#498, state: open
File Targets
- [x]
salt/states/stalwart/init.sls-- to create. Parent dirsalt/states/exists. No existing stalwart directory (clean). - [x]
salt/states/stalwart/config.json.j2-- to create. Same parent as above. - [x]
salt/pillar/stalwart.sls-- to create. Parent dirsalt/pillar/exists. No existing stalwart pillar. - [x]
salt/states/top.sls-- to modify. File exists. Currently assignscaddytoedge-proxyminion. Will addstalwartentry.
Pattern reference verified:
salt/states/caddy/containsinit.sls+Caddyfile.j2, pillar atsalt/pillar/caddy.sls. Stalwart structure mirrors this exactly.Repo Placement
OK. Issue filed on pal-e-platform, all file targets are in pal-e-platform's
salt/directory. Single-repo change.Dependencies
- Board #1732 (pal-e-platform#497, next_up): "Hetzner: Request port 25 outbound unblock" -- soft dependency. Stalwart can be installed and run before port 25 is unblocked; it just cannot send external mail. Install is not blocked.
- Board #1736 (pal-e-services#164, backlog): Stalwart domain/account config -- DEPENDS ON this ticket (#1733). Cannot configure Stalwart if it is not installed.
- Board #1740 (pal-e-services#168, todo): Deliverability validation -- depends on both #1733 and #1736 being complete.
Dependency chain is implicit from board ordering. No explicit blockers prevent this ticket from starting.
Acceptance Criteria
All 6 criteria are agent-verifiable:
salt-applyinstalls binary -- verifiable viawhich stalwartorstalwart --version- systemd service as dedicated user --
systemctl show stalwart --property=User - Port 587 listening --
ss -tlnp | grep 587(referenced in test expectations) - Port 8080 listening localhost only --
ss -tlnp | grep 8080 - Config from pillar -- inspect rendered config file contents
- Auto-restart on failure --
systemctl show stalwart --property=Restart
Test commands in "Test Expectations" section are real and executable.
Blast Radius
Low. New self-contained Salt state. Only shared file modified is
top.sls(adding one line). No port conflicts with existing Caddy service (80/443 vs 587/8080). No downstream consumers affected until subsequent tickets configure Stalwart.Decomposition Assessment
4 file targets in 1 repo. 6 acceptance criteria. Estimated agent time: 3-5 minutes (pattern-following work with caddy as reference). At upper boundary of 5-minute rule but acceptable -- all files follow one established pattern, no cross-repo coordination needed. No decomposition needed.
Recommendation
No action needed.
-
Review: Caddy reverse-proxy block for mail.landscaping-assistant.app
review-1734-2026-07-04Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type: Feature
- [x] Lineage
- [x] Repo
- [x] User Story
- [x] Context
- [x] File Targets
- [x] Feature Flag (none)
- [x] Acceptance Criteria
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related
All required sections present.
Traceability
- [x] story:self-hosted label — "As platform admin, I want zero third-party email dependencies so I control deliverability and avoid Gmail app password rotation"
- [x] story note verified — found in project-stalwart-email user-stories section (key: self-hosted, role: Platform Admin)
- [x] arch:edge-vps label — Hetzner Edge VPS component
- [x] arch note verified — arch-edge-vps note exists in pal-e-docs (Architecture: Hetzner Edge VPS)
- [x] Forgejo issue — ldraney/pal-e-platform#499, state: open
File Targets
- [x]
salt/pillar/caddy.sls— verified: file exists, contains caddy:sites pillar structure with domain/proxy_target/www_redirect keys - [ ]
salt/states/caddy/Caddyfile.j2— MISSING FROM TICKET: template hardcodes:443andtls_server_namefor ALL upstreams. Addingproxy_target: localhost:8080to the pillar would render asreverse_proxy localhost:8080:443withtls_server_name localhost:8080, which is broken. Template needs a conditional for local targets.
Repo Placement
Correct. Issue filed on ldraney/pal-e-platform, which contains both salt/pillar/caddy.sls and salt/states/caddy/Caddyfile.j2. Single-repo fix.
Dependencies
- Blocking: Issue #498 ("Salt: Install Stalwart binary + systemd service on edge VPS") — board item #1733, currently in backlog, state: open. Stalwart must be installed before the reverse proxy can route to it.
- Dependency is documented in the issue Lineage section.
Acceptance Criteria
3 AC + 1 test expectation = 4 total. All are verifiable by an agent post-implementation:
- "https://mail.landscaping-assistant.app loads Stalwart web admin" — curl + status code
- "TLS auto-provisioned by Caddy" — cert issuer check
- "Admin protected by Stalwart's built-in auth" — verify unauthenticated access returns 401/redirect
- curl -I returns 200/302 — straightforward
Missing AC: No regression criterion for existing sites after template change.
Blast Radius
HIGH concern: The Caddyfile.j2 template renders ALL sites. Introducing a conditional for local vs Tailscale targets must preserve backward compatibility for existing sites (palinks.app, landscaping-assistant.app, westsidekingsandqueens.com). A template error would break ALL public-facing services.
Decomposition Assessment
- 2 file targets in 1 repo — acceptable
- 4 acceptance criteria — under threshold
- Estimated agent work: under 5 minutes (pillar entry + template conditional)
- No decomposition needed
Recommendation
[BODY]Add missing file target:salt/states/caddy/Caddyfile.j2— needs conditional to skip:443suffix andtls_server_nametransport for local targets[BODY]Specify pillar schema extension: addlocal: trueflag to the mail site entry in caddy.sls to differentiate localhost targets from Tailscale targets[BODY]Add acceptance criterion: "Existing sites (palinks.app, landscaping-assistant.app, westsidekingsandqueens.com) continue to function after Caddyfile.j2 template change"
-
Review: Deliverability validation: mail-tester.com + end-to-end password reset (v2)
review-1740-2026-07-03-v2Verdict: APPROVED
Re-review after NEEDS_REFINEMENT. All previous issues resolved.
Template Completeness (Spike)
- [x] Type -- Spike
- [x] Lineage -- "Final gate -- depends on all other stalwart-email tickets"
- [x] Repo -- ldraney/pal-e-services
- [x] Question -- "Does our Stalwart setup achieve good deliverability..." with 5 sub-questions
- [x] Deliverables -- 4 concrete items with checkboxes
- [x] Time-box -- "2 hours"
- [x] Related -- stalwart-email project + blocker documentation
Traceability
- [x] story:password-reset label -- "As a user who forgot my password, I want to receive a reset email from my app's domain so I can regain access"
- [x] story note verified -- found in project-stalwart-email user-stories section
- [x] arch:stalwart label -- references Stalwart Mail Server component
- [x] arch note verified -- arch-stalwart note exists in pal-e-docs (created 2026-07-04, project: stalwart-email)
- [x] Forgejo issue -- ldraney/pal-e-services#168, state: open
File Targets
N/A -- Spike type. No code file targets expected. Deliverables are validation artifacts (score reports, test results) and follow-up tickets.
Repo Placement
OK. Issue filed on ldraney/pal-e-services. Repo section matches. Validation work spans infrastructure managed through pal-e-services (Stalwart config, Keycloak SMTP). No cross-repo mismatch.
Dependencies
Clearly documented: "Final gate -- depends on all other stalwart-email tickets." All 8 sibling board items remain in backlog:
- #1732 (pal-e-platform#497) -- edge-vps port 25, backlog
- #1733 (pal-e-platform#498) -- stalwart infra, backlog
- #1734 (pal-e-platform#499) -- edge-vps setup, backlog
- #1735 (pal-e-platform#500) -- PTR/rDNS, backlog
- #1736 (pal-e-services#164) -- stalwart config, backlog
- #1737 (pal-e-services#165) -- DNS landscaping, backlog
- #1738 (pal-e-services#166) -- DNS westside, backlog
- #1739 (pal-e-services#167) -- Keycloak SMTP, backlog
This ticket MUST NOT move to todo until all blockers complete. Dependencies are correctly documented.
Acceptance Criteria
4 deliverables (within the 5-item threshold). All are verifiable:
- mail-tester.com score report -- verifiable via browser automation (send test email, read score page)
- Password reset end-to-end -- verifiable via browser automation (click Forgot Password, check inbox, complete flow)
- Email header verification -- verifiable via email header inspection tools
- Follow-up tickets -- concrete artifact (create issues for anything below target)
Blast Radius
Minimal. Validation-only spike with no code changes. If deliverability issues are found, follow-up tickets route fixes to the appropriate config tickets. No downstream consumers affected by the validation itself.
Decomposition Assessment
- 4 deliverables (under >5 threshold)
- 0 file targets (spike -- validation only)
- 1 repo
- Sequential validation workflow -- all steps form one logical session
- Estimated agent time: within time-box
No decomposition needed.
Recommendation
No action needed. Scope is solid, traceability complete (all backing notes verified), fits in a single agent pass.
Minor Observation (non-blocking)
The Spike template suggests every spike produces a
docs/{topic}.mdfile. The deliverables list score reports and verification results rather than an explicit docs path. This is acceptable for a validation spike where the artifacts ARE the documentation, but the implementing agent should capture findings in a durable docs file (e.g.,docs/deliverability-validation.md).Previous Review Issues -- Resolution
- [RESOLVED] arch-stalwart note created -- now exists with full component diagram, key decisions, and related notes
- [RESOLVED] Type changed from Feature to Spike -- issue body now uses Spike template (Question/Deliverables/Time-box)
-
Review: DNS: SPF/DKIM/DMARC records for westsidekingsandqueens.com (re-review)
review-1738-2026-07-03-r2Verdict: READY
Re-review of board item #1738. All four issues from the prior review (review-1738-2026-07-03) have been resolved.
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- dependencies documented with board item IDs
- [x] Repo -- ldraney/pal-e-platform
- [x] User Story -- well-formed As/I want/So that
- [x] Context -- includes record types and values
- [x] File Targets -- terraform/dns.tf
- [x] Feature Flag -- none (infrastructure work)
- [x] Acceptance Criteria -- 5 criteria, all testable
- [x] Test Expectations -- dig commands provided
- [x] Constraints -- merge existing SPF, propagation window
- [x] Checklist -- standard checklist present
- [x] Related -- references stalwart-email project
Traceability
- [x] story:custom-domain label -- "As platform admin, I want each app to send from its own domain so emails look professional and build domain reputation"
- [x] story note verified -- found in project-stalwart-email user-stories section (custom-domain row)
- [x] arch:dns label -- DNS Management component
- [x] arch note verified -- arch-dns note exists in pal-e-docs (project: stalwart-email, created 2026-07-04)
- [x] Forgejo issue -- https://forgejo.tail5b443a.ts.net/ldraney/pal-e-services/issues/166, open
File Targets
- [x] terraform/dns.tf -- verified: exists at /home/ldraney/pal-e-platform/terraform/dns.tf (26 lines). Contains A records for palinks.app, landscaping-assistant.app, and westsidekingsandqueens.com using godaddy_dns_record resources. No SPF/DKIM/DMARC records yet -- clean addition.
Repo Placement
OK -- Issue filed on pal-e-services for project tracking; implementation in pal-e-platform. Explicitly documented in the issue body. File target confirmed in pal-e-platform codebase.
Dependencies
- Board #1733 (pal-e-platform#498): Stalwart install -- type:infra, arch:stalwart (backlog). Required before DKIM key extraction.
- Board #1736 (pal-e-services#164): Stalwart domain/account setup -- type:config, arch:stalwart (backlog). Produces the DKIM public key this ticket needs.
- Both dependencies are in backlog; this ticket is correctly also in backlog.
- Minor lineage typo: issue body says "pal-e-platform#498 / board #1736" but board #1736 is pal-e-services#164. Non-blocking (lineage is for humans per template spec).
Acceptance Criteria
5 criteria, all agent-verifiable via dig commands. Test commands are real and specific. The DKIM criterion requires the public key from Stalwart (dependency), which means final validation cannot happen until the dependency chain completes. This is acceptable -- the implementation can use a placeholder pending key extraction.
Blast Radius
- Board #1737 (pal-e-services#165) is the identical pattern for landscaping-assistant.app -- same labels, same structure. Changes here serve as the template for that sibling ticket.
- dns.tf is isolated -- only DNS records, no application code affected.
- No existing SPF/DKIM/DMARC records in the terraform directory for any domain.
Decomposition Assessment
No decomposition needed:
- 1 file target in 1 repo
- 5 acceptance criteria (at threshold but all in same file, same resource pattern)
- Estimated agent work: 2-3 minutes (adding 4 TXT resource blocks to existing file)
Observations (non-blocking)
- DKIM selector: issue says
default._domainkey, arch-dns note saysmail._domainkey. Will be resolved when Stalwart dependency produces the actual key. Agent should use whatever selector Stalwart generates. - Resource type: actual code uses
godaddy_dns_record; arch note saysgodaddy_domain_record. Minor arch note inaccuracy, does not affect implementation.
Recommendation
No action needed. Ticket is ready for implementation once dependency chain (Stalwart install -> Stalwart domain config) completes.
-
Review: Deliverability validation: mail-tester.com + end-to-end password reset
review-1740-2026-07-04Verdict: NEEDS_REFINEMENT
Re-review of board item #1740. Previous review (review-1740-2026-07-03) returned NEEDS_REFINEMENT for missing arch note and wrong type. Both issues are now fixed. However, the issue body was not reformatted to match the Spike template when the type was changed from Feature to Spike.
Template Completeness
Evaluated against
template-issue-spike. Issue declares### Type: Spikebut uses Feature template structure.- [x] Type -- Spike (fixed from previous review)
- [x] Lineage -- "Final gate -- depends on all other stalwart-email tickets"
- [x] Repo -- ldraney/pal-e-services
- [ ] Question -- MISSING. Has "User Story" + "Context" instead. Spike template requires a specific question this spike answers.
- [ ] Deliverables -- MISSING. Has "Acceptance Criteria" + "Checklist" instead. Spike template requires explicit deliverables (docs file + follow-up tickets).
- [ ] Time-box -- MISSING. Spike template requires maximum investigation time to prevent rabbit holes.
- [x] Related -- present
Extra sections from Feature template (not part of Spike template):
- User Story (spikes frame as Question, not User Story)
- File Targets (spikes implicitly target docs/{topic}.md)
- Feature Flag (not a spike section)
- Acceptance Criteria (spikes have Deliverables)
- Test Expectations (spikes explicitly have no test expectations per template)
- Constraints (not a standard spike section)
- Checklist (not a standard spike section)
Traceability
- [x] story:password-reset label -- "As a user who forgot my password, I want to receive a reset email from my app's domain so I can regain access"
- [x] story note verified -- found in project-stalwart-email user-stories section
- [x] arch:stalwart label -- present on board item
- [x] arch note verified -- arch-stalwart note exists in pal-e-docs (created 2026-07-04)
- [x] Forgejo issue -- ldraney/pal-e-services#168, open
File Targets
No code file targets -- appropriate for a validation spike. Expected spike output:
docs/deliverability-validation.md(not yet specified in issue body since Deliverables section is missing).Repo Placement
OK. Issue filed on ldraney/pal-e-services. Validation work runs against services deployed from this repo. No cross-repo mismatch.
Dependencies
Documented as "Final gate -- depends on all other stalwart-email tickets." All 8 sibling tickets confirmed in backlog:
- #1732 (pal-e-platform#497) -- edge-vps port 25 unblock, backlog
- #1733 (pal-e-platform#498) -- stalwart install, backlog
- #1734 (pal-e-platform#499) -- edge-vps PTR/rDNS, backlog
- #1735 (pal-e-platform#500) -- edge-vps custom-domain, backlog
- #1736 (pal-e-services#164) -- stalwart config, backlog
- #1737 (pal-e-services#165) -- DNS landscaping-assistant.app, backlog
- #1738 (pal-e-services#166) -- DNS westsidekingsandqueens.com, backlog
- #1739 (pal-e-services#167) -- Keycloak SMTP config, backlog
This ticket MUST NOT move to todo until all blockers complete. Dependencies correctly documented.
Acceptance Criteria
7 items present but in Feature template format. For a Spike, these should be reframed: the "acceptance criteria" are really the sub-questions/test cases the spike investigates. The actual deliverables are (1) a docs file recording results and (2) follow-up tickets if fixes are needed. Content is substantively correct but structurally misplaced.
Blast Radius
Minimal. No code changes. Validation-only spike. If deliverability issues are found, they route back to existing config tickets on the board. No downstream consumers affected.
Decomposition Assessment
- As a properly-formatted spike: 2 deliverables (docs file + follow-up tickets) -- under 5 threshold
- 0 code file targets
- 1 repo
- Single validation session
- Estimated agent time: ~5 minutes (external service calls, not complexity)
No decomposition needed. The 7 current "AC" items are sequential checks in one validation session -- they become sub-questions in the spike's Question section after reformatting.
Recommendation
- [BODY] Reformat issue body to use Spike template structure. Specifically:
- Replace
### User Story+### Contextwith### Question: "Does the stalwart email system achieve deliverability scores 9+/10 and successfully deliver end-to-end password resets?" with sub-questions for each test area. - Replace
### Acceptance Criteria+### Checklistwith### Deliverables: (1)docs/deliverability-validation.mddocumenting scores and test results, (2) follow-up tickets for any failures found. - Add
### Time-box: suggest "1 session" since all tests are sequential and external-service dependent. - Remove
### Feature Flag,### File Targets,### Test Expectations,### Constraints(incorporate relevant constraints into Question context).
- Replace
Traceability is now complete (arch-stalwart created, story verified). Only the body structure remains to be fixed.
-
Review: DNS: SPF/DKIM/DMARC records for landscaping-assistant.app
review-1737-2026-07-03-r2Verdict: READY
Re-review of board item #1737 after refinement. All three issues from the previous review (review-1737-2026-07-03) are resolved.
Template Completeness
- [x] Type (Feature)
- [x] Lineage
- [x] Repo (ldraney/pal-e-platform)
- [x] User Story
- [x] Context
- [x] File Targets (terraform/dns.tf)
- [x] Feature Flag (none)
- [x] Acceptance Criteria
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related
All sections present. Template structurally complete.
Traceability
- [x] story:custom-domain label — "As platform admin, I want each app to send from its own domain so emails look professional and build domain reputation"
- [x] story note verified — found in project-stalwart-email user-stories section (key: custom-domain, role: Platform Admin)
- [x] arch:dns label — DNS component
- [x] arch note verified — arch-dns note exists in pal-e-docs (created 2026-07-04, project: stalwart-email)
- [x] Forgejo issue — ldraney/pal-e-services#165, open
File Targets
- [x]
terraform/dns.tf— verified: file exists in pal-e-platform, currently contains A records for landscaping-assistant.app (lines 12-18, resource "godaddy_dns_record" "landscaping_assistant_a"). SPF/DKIM/DMARC resources will be appended here using the same resource type pattern.
Repo Placement
OK. Issue filed on pal-e-services for project tracking. Body explicitly states
### Repo: ldraney/pal-e-platformand includes note: "Issue filed on pal-e-services for project tracking; implementation work is in pal-e-platform." The GoDaddy provider and existing DNS records live in pal-e-platform/terraform/. Cross-repo tracking is intentional and documented.Dependencies
- Documented: "Depends on: Stalwart domain/account setup (needs DKIM public key)" = pal-e-services#164 (board item #1736)
- Transitive: #1736 depends on pal-e-platform#498 (Stalwart install, board item #1733)
- Chain: #1733 (Stalwart install) → #1736 (domain/account setup, generates DKIM key) → #1737 (this item)
- Both upstream items in backlog — this item is correctly blocked until DKIM public key is generated. Dependency is explicitly documented in Lineage section.
Acceptance Criteria
5 criteria, all verifiable programmatically:
- [x] A record: verifiable via
dig A mail.landscaping-assistant.app - [x] SPF: verifiable via
dig TXT landscaping-assistant.app +short - [x] DKIM: verifiable via
dig TXT default._domainkey.landscaping-assistant.app +short - [x] DMARC: verifiable via
dig TXT _dmarc.landscaping-assistant.app +short - [x] mxtoolbox validation: verifiable via web check (24-48h propagation caveat noted in Constraints)
All criteria are clear, testable, and within agent capability. DKIM criterion naturally gated by Stalwart dependency.
Blast Radius
- Sibling domain: Issue #166 (board item #1738) is the identical pattern for westsidekingsandqueens.com — no conflict, independent work
- Third domain: palinks.app has an A record in dns.tf (lines 4-10) but no SPF/DKIM/DMARC — may need similar treatment in the future but out of scope for this ticket
- Single SPF constraint: Issue correctly notes "Only ONE SPF record per domain" — no existing SPF record for landscaping-assistant.app in dns.tf, so no conflict risk
- Provider pattern: Uses
godaddy_dns_recordresource type consistent with existing resources in the file
Decomposition Assessment
1 file target (terraform/dns.tf), 4 DNS resources to add (A for mail subdomain, SPF TXT, DKIM TXT, DMARC TXT), 5 acceptance criteria. Estimated agent work: under 5 minutes once DKIM key is available. No decomposition needed.
Previous Review Issues — Resolution Status
- [x] Wrong repo — FIXED:
### Reponow correctly statesldraney/pal-e-platform - [x] Vague file targets — FIXED: File Targets now specifies
terraform/dns.tf - [x] Missing arch-dns note — FIXED:
arch-dnsnote created (Architecture: DNS Management)
Recommendation
No action needed. Scope is solid, all file targets verified, traceability complete, fits in a single agent pass.
Minor Observation (non-blocking)
The DKIM selector in the issue body uses
default._domainkeywhile the arch-dns note referencesmail._domainkey. The actual selector will be determined by Stalwart's configuration during dependency #1736. The implementing agent should use whatever selector Stalwart generates rather than hardcoding either assumption. This is inherent to the dependency relationship and does not require ticket changes. -
Review: Hetzner: Set PTR/rDNS for mail.landscaping-assistant.app (v2)
review-1735-2026-07-03-v2Verdict: APPROVED
Re-review after NEEDS_REFINEMENT (review-1735-2026-07-03). Both issues resolved: AC4 (HELO/EHLO) removed, arch-edge-vps note created.
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- present, documents dependencies (Stalwart install, port 25 unblock)
- [x] Repo -- ldraney/pal-e-platform
- [x] User Story -- well-formed (As a platform admin / I want correct PTR/rDNS / So that outbound email passes FCrDNS)
- [x] Context -- explains FCrDNS requirement clearly
- [x] File Targets -- 1 manual action (Hetzner Console) + 1 code file (terraform comment)
- [x] Feature Flag -- none (appropriate for infra work)
- [x] Acceptance Criteria -- 3 items, all verifiable via dig
- [x] Test Expectations -- mxtoolbox FCrDNS validation
- [x] Constraints -- single PTR per IP acknowledged, A record dependency noted
- [x] Checklist -- present
- [x] Related -- present (stalwart-email project)
Traceability
- [x] story:custom-domain label -- "As platform admin, I want each app to send from its own domain so emails look professional and build domain reputation"
- [x] story note verified -- found in project-stalwart-email user-stories section (key: custom-domain)
- [x] arch:edge-vps label -- references the Hetzner edge VPS component
- [x] arch note verified -- arch-edge-vps note exists in pal-e-docs (project: stalwart-email, type: architecture)
- [x] Forgejo issue -- ldraney/pal-e-platform#500, open
File Targets
- [x] terraform/modules/hetzner-edge/main.tf -- verified: file exists at /home/ldraney/pal-e-platform/terraform/modules/hetzner-edge/main.tf, contains hcloud_server "edge" resource (60 lines). No PTR/rDNS comments exist yet; ticket correctly scopes adding them.
- [x] Manual: Hetzner Cloud Console rDNS -- not a file target, manual action appropriate for Hetzner panel PTR setting.
Repo Placement
Correct. Issue filed on pal-e-platform which owns the terraform code. Manual Hetzner Console action is appropriate here. No cross-repo concerns now that AC4 (HELO/EHLO) has been removed.
Dependencies
- Board item #1732 (issue #497) -- "Hetzner: Request port 25 outbound unblock" (backlog, arch:edge-vps). Prerequisite documented in Lineage.
- Board item #1733 (issue #498) -- Stalwart install (backlog, arch:stalwart). Prerequisite documented in Lineage.
- Board item #1737 or #1738 (pal-e-services DNS tickets) -- A record for mail.landscaping-assistant.app. Dependency documented in Constraints section.
All prerequisites documented. All prerequisite items remain in backlog -- ordering is correct (this ticket should not start until dependencies complete).
Acceptance Criteria
3 criteria, all verifiable via dig commands:
- AC1: PTR resolves to mail.landscaping-assistant.app -- verifiable via
dig -x 178.156.129.142 - AC2: A record resolves back to the IP -- verifiable via
dig mail.landscaping-assistant.app(verifies FCrDNS forward leg; depends on DNS ticket completion) - AC3: Specific dig command with expected output -- directly executable
Test expectation (mxtoolbox FCrDNS check) is valid and automatable. All criteria are agent-verifiable post-implementation.
Blast Radius
Only ONE PTR per IP (178.156.129.142). Multiple domains share this IP (palinks.app, landscaping-assistant.app, westsidekingsandqueens.com). Constraints section correctly acknowledges this and selects mail.landscaping-assistant.app as canonical. No downstream consumers affected -- PTR is for outbound mail identity only. No existing hcloud_rdns resources in the codebase.
Decomposition Assessment
1 code file target + 1 manual action. 3 acceptance criteria. Estimated agent work well under 5 minutes (add terraform comment + manual Hetzner Console action). No decomposition needed.
Recommendations
No action needed.
-
Review: Hetzner: Request port 25 outbound unblock
review-1732-2026-07-04Verdict: READY
Board item #1732 on board-stalwart-email. Forgejo issue: ldraney/pal-e-platform#497. Re-review after arch-edge-vps note creation.
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- Standalone, part of stalwart-email project
- [x] Repo -- ldraney/pal-e-platform
- [x] User Story -- As a platform admin, I want outbound port 25 unblocked...
- [x] Context -- Explains Hetzner's 30-day anti-spam block, low-volume transactional use case
- [x] File Targets -- "No code changes" (manual action, appropriate)
- [x] Feature Flag -- none (correct, no code changes)
- [x] Acceptance Criteria -- 3 items, concrete
- [x] Test Expectations -- Manual SSH verification command
- [x] Constraints -- Timing, request wording, inbound port warning
- [x] Checklist -- Present
- [x] Related -- stalwart-email project referenced
Traceability
- [x] story:self-hosted label -- "As platform admin, I want zero third-party email dependencies so I control deliverability and avoid Gmail app password rotation"
- [x] story note verified -- found in project-stalwart-email user-stories section (key: self-hosted)
- [x] arch:edge-vps label -- references the Hetzner Edge VPS component
- [x] arch note verified -- arch-edge-vps note exists in pal-e-docs (project: stalwart-email, created 2026-07-04)
- [x] Forgejo issue -- ldraney/pal-e-platform#497, state: open
File Targets
No code changes -- this is a manual action in Hetzner Cloud Console. No file verification needed. Appropriate for the scope of work (submitting an external vendor request).
Repo Placement
OK. Issue is filed on pal-e-platform and the Repo section correctly identifies ldraney/pal-e-platform. Platform infrastructure work belongs here. No cross-repo concerns.
Dependencies
Issue Context explicitly states: "This is a blocking dependency for all other Stalwart work." Board confirms multiple downstream items depend on port 25:
- #1733 (pal-e-platform#498) -- story:self-hosted, arch:stalwart (likely Stalwart deployment)
- #1734 (pal-e-platform#499) -- story:self-hosted, arch:edge-vps
- #1735 (pal-e-platform#500) -- story:custom-domain, arch:edge-vps (PTR/rDNS)
- #1736, #1739, #1740 -- downstream Stalwart config and Keycloak integration
Dependency is well-documented in scope. No undocumented blockers found.
Acceptance Criteria
3 criteria, all verifiable:
- "Request submitted" -- verifiable by human (screenshot/confirmation email)
- "Request approved" -- verifiable by Hetzner response
- "telnet gmail-smtp-in.l.google.com 25 from edge VPS" -- concrete, automatable test command
Criteria are appropriate for a manual vendor-request ticket. An agent can verify criterion 3 via SSH.
Blast Radius
Minimal. No code changes, no downstream breakage risk. The unblock only enables outbound SMTP on port 25; inbound is explicitly excluded per Constraints. No sibling services affected.
Decomposition Assessment
No decomposition needed:
- 0 file targets (manual action)
- 3 acceptance criteria (under 5 threshold)
- Agent time: minimal (human-dependent wait for Hetzner approval)
Recommendation
No action needed. Scope is complete and well-defined.
-
Review: DNS: SPF/DKIM/DMARC records for landscaping-assistant.app
review-1737-2026-07-03Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type (Feature)
- [x] Lineage
- [x] Repo
- [x] User Story
- [x] Context
- [x] File Targets
- [x] Feature Flag (none)
- [x] Acceptance Criteria
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related
All sections present. Template structurally complete.
Traceability
- [x] story:custom-domain label — "As platform admin, I want each app to send from its own domain so emails look professional and build domain reputation"
- [x] story note verified — found in project-stalwart-email user-stories section
- [x] arch:dns label — DNS component
- [ ] arch note MISSING — [SCOPE] Create architecture note arch-dns for component dns
- [x] Forgejo issue — ldraney/pal-e-services#165, open
File Targets
- [ ] "GoDaddy DNS configuration (manual or via Terraform GoDaddy provider if available)" — ISSUE: Vague. The actual target is
pal-e-platform/terraform/dns.tf. The GoDaddy provider (ldraney/godaddy v0.1.0) IS available and already manages A records for this domain. Should specify the exact file path.
Repo Placement
MISMATCH. Issue specifies
### Repo: ldraney/pal-e-servicesand is filed on that repo. However:- DNS records are managed in
pal-e-platform/terraform/dns.tf - The GoDaddy provider is configured in
pal-e-platform/terraform/providers.tf - The existing A record for landscaping-assistant.app already lives in
pal-e-platform/terraform/dns.tf(line 12-18) pal-e-serviceshas NO GoDaddy provider and no DNS management capability
The work must be done in
ldraney/pal-e-platform. Either the issue should be moved or the body should acknowledge cross-repo work.Dependencies
- Documented: "Depends on: Stalwart domain/account setup (needs DKIM public key)" = pal-e-services#164 (board item #1736)
- Transitive: #164 depends on pal-e-platform#498 (Stalwart install, board item #1733)
- Chain: #1733 (Stalwart install) → #1736 (domain/account setup) → #1737 (this item)
- Both upstream items in backlog — this item is blocked until DKIM public key is generated
Acceptance Criteria
5 criteria, all verifiable programmatically:
- A record: verifiable via
dig A mail.landscaping-assistant.app - SPF: verifiable via
dig TXT landscaping-assistant.app - DKIM: verifiable via
dig TXT default._domainkey.landscaping-assistant.app - DMARC: verifiable via
dig TXT _dmarc.landscaping-assistant.app - mxtoolbox validation: verifiable via web check (24-48h propagation caveat)
Criteria are clear and testable. Note: DKIM criterion cannot be verified until Stalwart generates the key (dependency).
Blast Radius
- Sibling domain: Issue #166 (board item #1738) is the identical pattern for westsidekingsandqueens.com
- Third domain: palinks.app has an A record in dns.tf (line 4-9) but no SPF/DKIM/DMARC — may need the same treatment
- Single SPF constraint: Issue correctly notes "Only ONE SPF record per domain" — if palinks.app already has an SPF record elsewhere, adding one via Terraform could conflict
Decomposition Assessment
1 file to modify (dns.tf), 4 DNS resources to add, 5 acceptance criteria. Estimated agent work: under 5 minutes once DKIM key is available. No decomposition needed.
Recommendation
[BODY]Fix repo reference:### Reposhould beldraney/pal-e-platform(notpal-e-services)[BODY]Fix file targets: replace vague "GoDaddy DNS configuration" withterraform/dns.tf(append SPF/DKIM/DMARC resources after existing landscaping_assistant_a resource at line 18)[SCOPE]Decide issue placement: move issue to pal-e-platform repo, or document in body that this is cross-repo work targeting pal-e-platform[SCOPE]Create architecture note arch-dns for the DNS component
-
Review: DNS: SPF/DKIM/DMARC records for westsidekingsandqueens.com
review-1738-2026-07-03Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type (Feature)
- [x] Lineage
- [x] Repo
- [x] User Story
- [x] Context
- [x] File Targets (present but vague -- see below)
- [x] Feature Flag
- [x] Acceptance Criteria
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related
Traceability
- [x] story:custom-domain label -- "As platform admin, I want each app to send from its own domain so emails look professional and build domain reputation"
- [x] story note verified -- found in project-stalwart-email user-stories section
- [x] arch:dns label -- DNS component
- [ ] arch note MISSING -- [SCOPE] Create architecture note arch-dns for DNS component
- [x] Forgejo issue -- https://forgejo.tail5b443a.ts.net/ldraney/pal-e-services/issues/166, open
File Targets
- [ ] "GoDaddy DNS configuration" -- ISSUE: Not an actual file path. DNS A records for westsidekingsandqueens.com already exist in
pal-e-platform/terraform/dns.tf(lines 20-26). SPF/DKIM/DMARC records should logically go in the same file. However, no email auth DNS records exist for ANY domain in IaC (including landscaping-assistant.app), suggesting these may be managed manually via GoDaddy web UI.
Repo Placement
MISMATCH: Issue is filed on
ldraney/pal-e-servicesbut DNS records are managed inpal-e-platform/terraform/dns.tf. The pal-e-services repo handles service automation (ArgoCD, Keycloak, Harbor, CNPG, databases) -- not DNS records. The A record for westsidekingsandqueens.com already exists in pal-e-platform. If SPF/DKIM/DMARC records will be managed via IaC, they belong in pal-e-platform. If they are manual GoDaddy UI operations, the issue body should explicitly state that and the file targets section should say "No IaC changes -- manual GoDaddy UI configuration."Dependencies
- Lineage states: "Depends on: Stalwart domain/account setup (needs DKIM public key)"
- Board item #1736 (pal-e-services#164): "Stalwart: Create domains + SMTP accounts via API" -- this generates the DKIM public key needed for this ticket. Currently in backlog.
- Board item #1733 (pal-e-platform#498): "Salt: Install Stalwart binary + systemd service on edge VPS" -- prerequisite to #1736. Currently in backlog.
- Dependency chain: #1733 -> #1736 -> #1738 (this ticket). Both dependencies are in backlog. The DKIM public key cannot be obtained until Stalwart is installed and configured.
Acceptance Criteria
5 criteria -- all verifiable with standard DNS dig commands and mxtoolbox.com. Criteria are concrete and agent-testable. However, DKIM criterion requires the public key from Stalwart (dependency #1736), making this ticket unimplementable until that dependency is resolved.
Blast Radius
Low. Changes affect only westsidekingsandqueens.com DNS records. No downstream services affected. Constraint about merging with existing SPF record (possible Gmail SPF) is appropriately noted.
Decomposition Assessment
1 logical file target, 5 acceptance criteria, single domain scope. Estimated agent time under 5 minutes once dependency resolved and repo/target clarified. No decomposition needed.
Recommendations
- [BODY] Fix Repo field: change from
ldraney/pal-e-servicestoldraney/pal-e-platformif records will be IaC-managed, OR clarify in File Targets that this is manual GoDaddy UI work with no code changes. - [BODY] Fix File Targets: replace "GoDaddy DNS configuration" with either
terraform/dns.tf(in pal-e-platform) or "Manual: GoDaddy DNS web UI (no IaC file changes)". - [BODY] Clarify "Same pattern as landscaping-assistant.app" -- no SPF/DKIM/DMARC records exist in IaC for any domain. If that pattern was manual, state it explicitly so the agent knows the approach.
- [BODY] Add explicit dependency reference: "Blocked by board item #1736 (pal-e-services#164: Stalwart domain/account setup)" so ordering is machine-readable.
- [SCOPE] Create architecture note arch-dns for DNS component (previously flagged in review-1624-2026-06-28).
-
Review: Hetzner: Request port 25 outbound unblock
review-1732-2026-07-03Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type -- Feature
- [x] Lineage
- [x] Repo
- [x] User Story
- [x] Context
- [x] File Targets -- "No code changes" (manual action, acceptable)
- [x] Feature Flag -- "none" (correct for infra work)
- [x] Acceptance Criteria
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related
Traceability
- [x] story:self-hosted label -- "As platform admin, I want zero third-party email dependencies so I control deliverability and avoid Gmail app password rotation"
- [x] story note verified -- found in project-stalwart-email user-stories section
- [x] arch:edge-vps label -- references the Hetzner edge VPS component
- [ ] arch note MISSING -- [SCOPE] Create architecture note arch-edge-vps for component edge-vps
- [x] Forgejo issue -- ldraney/pal-e-platform#497, open
File Targets
N/A -- issue explicitly states "No code changes -- this is a manual action in Hetzner Cloud Console." Appropriate for a type:infra issue that involves a provider console request.
Repo Placement
OK -- filed on ldraney/pal-e-platform which is the correct home for platform-level infrastructure issues. No multi-repo concerns.
Dependencies
This ticket self-identifies as a blocking dependency: "This is a blocking dependency for all other Stalwart work." Confirmed by board inspection -- items #1733 (Stalwart install, arch:stalwart, 5pts), #1736 (Stalwart config, arch:stalwart, 3pts), and #1740 (validation, arch:stalwart) all depend on port 25 being open. Dependencies are documented in the Context section. No undocumented blockers found.
Acceptance Criteria
3 criteria, all verifiable:
- "Port 25 unblock request submitted" -- manual verification in Hetzner console
- "Request approved by Hetzner" -- confirmable via console status or email notification
- "Verified with: telnet gmail-smtp-in.l.google.com 25 from edge VPS" -- concrete, automatable test command
AC are clear and testable. An agent can verify criterion 3 via SSH.
Blast Radius
Minimal. This is a network-level change (outbound port 25 unblock) on the Hetzner edge VPS. No code changes, no configuration drift risk. The constraint "DO NOT open inbound port 25" is explicitly documented, preventing accidental exposure. No sibling services affected.
Decomposition Assessment
No decomposition needed. Zero file targets, 3 acceptance criteria, single-domain action (Hetzner console). The async wait for Hetzner approval is external and does not increase agent complexity. Well under the 5-minute rule.
Recommendation
- [SCOPE] Create architecture note arch-edge-vps for the Hetzner edge VPS component. This note should document: the VPS IP (178.156.129.142), its role in the platform (mail server, reverse proxy), managed-by (SaltStack), and services running on it (Stalwart, Caddy).
Architecture 3
-
Architecture: Stalwart Mail Server
arch-stalwart## Deployment: Stalwart Mail Server Stalwart Mail Server — Rust single-binary mail server handling SMTP relay for transactional email (password resets, notifications). ### Diagram ```mermaid graph TB subgraph Internet Recipient[Recipient Mail Servers] Bounce[Bounce Messages] end subgraph Hetzner["Hetzner Edge VPS"] Caddy[Caddy :443] subgraph Stalwart["Stalwart Mail Server"] SMTP_In[SMTP :25] Submission[Submission :587] IMAP[IMAPS :993] Admin[Admin UI :8080] DKIM[DKIM Signing] end end subgraph k3s["Home k3s Cluster"] Keycloak[Keycloak Realms] end Keycloak -->|sends via port 587| Submission Submission --> DKIM DKIM -->|signed email| Recipient Bounce -->|port 25| SMTP_In Caddy -->|reverse proxy| Admin ``` ### Components | Component | Purpose | Notes | |-----------|---------|-------| | Stalwart SMTP (port 25) | Inbound relay for bounces | Receives NDRs and feedback loops | | Stalwart Submission (port 587) | Authenticated outbound sending | Keycloak connects here per-realm | | Stalwart IMAPS (port 993) | Optional mailbox access | For checking bounce mailboxes | | Stalwart Admin UI (port 8080) | Web management interface | Behind Caddy reverse proxy | | DKIM Signing | Cryptographic email authentication | Per-domain key pairs | | Keycloak | Identity provider sending transactional email | Each realm has own from-address | ### Key Decisions - **Stalwart over alternatives**: Single binary, ~50MB RAM; no Java/Elasticsearch dependencies - **NOT on k3s**: Requires port 25 + PTR/rDNS on the sending IP — must run on Hetzner VPS - **SaltStack managed**: Declarative state in `salt/states/stalwart/`; pillar-driven config - **Per-realm SMTP config**: Each Keycloak realm sends from its own domain (e.g., noreply@landscaping-assistant.app) - **DKIM per domain**: Keys generated by Stalwart, public key extracted for DNS records ### Related - `arch-edge-vps` — host VPS where Stalwart runs - `arch-dns` — DNS records (SPF/DKIM/DMARC) for mail deliverability - Project: `stalwart-email` ## Integration with Keycloak Keycloak realms connect via SMTP submission (port 587): - Config lives in `~/secrets/pal-e-services/k3s.tfvars` under `keycloak_realms.{realm}.smtp` - Terraform `dynamic "smtp_server"` block in `terraform/keycloak.tf` (lines 47-63) - Each realm gets its own from-address (e.g., noreply@landscaping-assistant.app) ## Per-Domain Setup Pattern For each new sending domain: 1. Create domain + account in Stalwart admin UI 2. Extract DKIM public key 3. Add SPF/DKIM/DMARC DNS records 4. Add `smtp = {...}` block to realm in k3s.tfvars 5. Apply terraform to configure Keycloak ## Key Files - `salt/states/stalwart/init.sls` — install + configure state - `salt/pillar/stalwart.sls` — domains, accounts, DKIM keys - `salt/states/top.sls` — assigns stalwart state to edge-proxy - `terraform/keycloak.tf` — dynamic smtp_server block
-
Architecture: DNS Management
arch-dns## Deployment: DNS Management DNS records for all pal-e platform domains, managed via OpenTofu with the GoDaddy provider. ### Diagram ```mermaid graph TB subgraph GoDaddy["GoDaddy DNS"] A_Records[A Records] TXT_SPF[SPF TXT Records] TXT_DKIM[DKIM TXT Records] TXT_DMARC[DMARC TXT Records] end subgraph OpenTofu["OpenTofu IaC"] DNS_TF[terraform/dns.tf] Provider[ldraney/godaddy provider v0.1.0] end subgraph Hetzner["Hetzner"] PTR[PTR/rDNS Record] VPS[Edge VPS 178.156.129.142] end DNS_TF --> Provider Provider --> A_Records Provider --> TXT_SPF Provider --> TXT_DKIM Provider --> TXT_DMARC A_Records -->|resolves to| VPS PTR -->|reverse lookup| VPS ``` ### Components | Component | Purpose | Notes | |-----------|---------|-------| | GoDaddy DNS | Authoritative nameserver for all domains | Registrar + nameserver hosting | | OpenTofu | IaC for DNS record management | `terraform/dns.tf` with `godaddy_domain_record` resources | | A Records | Point domains to edge VPS | All resolve to 178.156.129.142 | | SPF Record | Authorize sending IP | `v=spf1 ip4:178.156.129.142 -all` | | DKIM Record | Cryptographic signing verification | `mail._domainkey` → public key from Stalwart | | DMARC Record | Policy for failed auth | `v=DMARC1; p=reject; rua=mailto:...` | | PTR/rDNS | Reverse DNS for deliverability | Managed via Hetzner console, not Terraform | ### Key Decisions - **GoDaddy as registrar + DNS**: All domains already registered here; consolidates management - **OpenTofu for DNS IaC**: Reproducible, version-controlled record management - **Separate PTR management**: PTR records belong to IP owner (Hetzner), not domain registrar - **Strict SPF (-all)**: Hard-fail policy for maximum deliverability reputation - **DMARC reject policy**: Prevents domain spoofing; monitors via `rua` aggregate reports ### Related - `arch-edge-vps` — the VPS that A records point to - `arch-stalwart` — generates DKIM keys referenced in DNS - Project: `stalwart-email` ## Domains | Domain | Purpose | A Record Target | |--------|---------|-----------------| | landscaping-assistant.app | Landscaping app (public) | 178.156.129.142 | | westsidekingsandqueens.com | Basketball league app | 178.156.129.142 | | palinks.dev | Link shortener | 178.156.129.142 | ## IaC Location - **File**: `terraform/dns.tf` in `pal-e-platform` - **Provider**: `ldraney/godaddy` v0.1.0 - **Pattern**: `godaddy_domain_record` resources per record type ## Key Constraints - GoDaddy API has rate limits — batch changes where possible - PTR records are managed by Hetzner (IP owner), not GoDaddy - DKIM key is generated by Stalwart — must be extracted after install
-
Architecture: Hetzner Edge VPS
arch-edge-vps## Deployment: Hetzner Edge VPS Hetzner CAX11 (cpx11) VPS at 178.156.129.142 serving as the internet-facing edge proxy and mail relay for the pal-e platform. ### Diagram ```mermaid graph TB subgraph Internet DNS[DNS A Records] Client[External Clients] end subgraph Hetzner["Hetzner Edge VPS (178.156.129.142)"] Caddy[Caddy :80/:443] Stalwart[Stalwart :25/:587/:993/:8080] TS_Edge[Tailscale Agent] end subgraph Home["Home k3s Cluster"] TS_K3s[Tailscale Agent] Apps[Application Pods] end DNS --> Client Client -->|HTTPS| Caddy Client -->|SMTP| Stalwart Caddy -->|reverse proxy| TS_Edge TS_Edge -->|mesh VPN| TS_K3s TS_K3s --> Apps Stalwart -->|submission| Apps ``` ### Components | Component | Purpose | Notes | |-----------|---------|-------| | Caddy | Reverse proxy + TLS termination | Ports 80, 443; auto-HTTPS via ACME | | Stalwart | SMTP relay, submission, IMAP, admin UI | Ports 25, 587, 993, 8080 | | Tailscale | Mesh VPN to k3s cluster | Connects edge to home network | | Hetzner Firewall | Cloud-level packet filtering | Ports 22, 80, 443, 25, 587, 993 | ### Key Decisions - **Edge VPS instead of k3s for mail**: Home ISPs block port 25 outbound; Hetzner allows it after unblock request - **PTR/rDNS on Hetzner IP**: Required for mail deliverability; only available on static IPs with hosting provider control - **Public IP stability**: Hetzner provides a fixed IPv4 that DNS A records point to — no DDNS needed - **SaltStack for config management**: Declarative state for Caddy + Stalwart; pillar-driven config - **OpenTofu for provisioning**: Server, firewall, SSH key managed as IaC ### Related - `arch-dns` — DNS records pointing to this VPS - `arch-stalwart` — Stalwart mail server running on this VPS - Project: `stalwart-email` ## Management - **Provisioning**: OpenTofu (`terraform/modules/hetzner-edge/main.tf`) - **Configuration**: SaltStack (`salt/states/top.sls` → `edge-proxy` minion) - **Firewall**: Hetzner cloud firewall (ports 22, 80, 443) + port 25/587/993 for mail - **Connectivity**: Tailscale for internal cluster access ## Key Files - `terraform/modules/hetzner-edge/main.tf` — server, firewall, SSH key - `salt/states/caddy/` — Caddy state + Caddyfile.j2 template - `salt/states/top.sls` — assigns states to `edge-proxy` minion - `salt/pillar/caddy.sls` — site definitions (domain + proxy_target)