Skip to content

Offer to email the anonymous report, as a softer alternative to gating it #64

Description

@georgekaran

An alternative to #59's gate, or a step before it. The two are in tension and the tension is the point of this issue.

Context

#59 proposes converting anonymous visitors by withholding: reveal two or three findings, lock the rest behind signup. It is a real reversal — today an anonymous audit returns the complete result, and share links are fully public — and it trades goodwill for conversion.

There is a softer instrument that captures the same lead without taking anything away: offer to email the report. The person keeps the full result on screen; some fraction of them hand over an address to keep it. Nothing is hidden, nothing is reversed, and the ones who decline are not worse off than they are today.

We already have the delivery half. The alert work shipped a Resend adapter, a durable outbox, provider retries and one-click unsubscribe.

Scope

In

  • An optional email field on the anonymous result: "Email me this report."
  • One message containing the audited URL, score, counts by impact, and a link to the existing public result.
  • Reuse of the existing outbox and sender rather than a second delivery path.
  • Unsubscribe and suppression handling consistent with the alert mail.

Out

This endpoint is an abuse vector, and that shapes the design

An unauthenticated endpoint that sends mail to an arbitrary address is a mail bomb primitive: paste victim@example.com, submit repeatedly, and we are the ones delivering it. Our domain's reputation is the collateral, and reputation lost this way is slow to rebuild.

So the constraints are not optional extras:

  • No user-controlled content in the message. The body is our template plus an audit id. No free-text field, no reply-to, no "add a note".
  • One send per audit. The message is a receipt for a specific result, not a resend button.
  • Rate limited on the address as well as the IP. The existing per-IP buckets bound one source; the thing worth bounding here is how often any single address can be targeted, which is a per-recipient bucket. makeRateLimit already supports several rules per route — the signup path does exactly this with IP and email.
  • Suppression is honoured before sending. Someone who unsubscribed from alerts must not receive this.

Worth saying plainly: if these are inconvenient to build, that is an argument for not shipping the feature, not for shipping it without them.

What this is really asking

Both instruments capture leads; they cost different things.

Withholding (#59) Email capture (this)
What the visitor gets Part of the result All of it
What we get An account, or nothing An address, or nothing
Reverses a shipped decision Yes No
Share-link leak Yes — an incognito window defeats it No
Abuse surface None new Real, see above

They are not exclusive. The sequence worth considering is this one first — it is cheaper, reversible, and measurable — with #59 as the harder move if the numbers say the soft one is not enough. Deciding that is the actual value of this issue; the implementation is small.

Acceptance criteria

  • An anonymous result offers to email itself, and the on-screen result is unchanged whether or not the offer is taken.
  • The message body contains no user-supplied content beyond the audited URL.
  • Per-recipient and per-IP rate limits both apply, with a spec for each.
  • Existing unsubscribe and suppression state is honoured.
  • Delivery goes through the existing outbox, with no second sender path.
  • Whether this replaces, precedes, or accompanies v2: mark findings on the user's own page, and gate the rest behind signup #59 is recorded in DECISIONS.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:serverBackend API and domain code under server/area:webFrontend under web/enhancementNew feature or requesttype:decisionRequires a technical choice, to be logged in DECISIONS.md

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions