@kurtbruns/kestrel 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ Kestrel uses two hostnames: one for the app, and one your email is sent from.
22
22
 
23
23
  These guides put the app on a subdomain, which leaves any website you already have on `example.com` untouched. If `example.com` has no website of its own, the app can live there instead. Use `example.com` wherever these guides say `newsletter.example.com`.
24
24
 
25
- It's recommended to send from a subdomain rather than from `example.com` itself. Your apex domain carries the reputation of your regular email, and a newsletter shouldn't put that at risk. Avoid `mail.` as the sending name too, since mail servers read it as an inbound host. [Sending-domain DNS](../reference/02-sending-domain-dns.md#choose-your-sending-name) explains both.
25
+ It's recommended to send from a subdomain rather than from `example.com` itself. Your apex domain carries the reputation of your regular email, and a newsletter shouldn't put that at risk. These guides assume a sending subdomain throughout. Avoid `mail.` as the sending name too, since mail servers read it as an inbound host. [Sending-domain DNS](../reference/02-sending-domain-dns.md#choose-your-sending-name) explains both.
26
26
 
27
27
  Throughout these guides, replace the example names with your own, and any value written as `REPLACE_WITH_…`.
28
28
 
@@ -175,17 +175,15 @@ npx wrangler deploy --env production
175
175
 
176
176
  This deploys the release your instance runs, with your settings. The output ends with the hostname the app now answers on. Deploying also registers the once-a-minute schedule that sends posts, which you can see under the Worker's **Triggers** tab in the Cloudflare dashboard.
177
177
 
178
- Always name `--env production`, here and on every wrangler command that stores a secret. Without it, wrangler uses the top level, which is local development. A deploy then creates a second Worker named `kestrel`, which runs with an email stand-in that delivers nothing and a sign-in meant only for your machine. The top level turns off every address it could answer on and the schedule that sends posts, so no one can reach it and it never sends, but it does nothing useful. If you deployed without `--env production`, remove that Worker and deploy again with it:
178
+ Always name `--env production`, here and on every wrangler command that stores a secret. Without it, wrangler deploys the top level, local development, as a second Worker named `kestrel` that answers nowhere and never sends. If that happens, remove it:
179
179
 
180
180
  ```bash
181
181
  npx wrangler delete --name kestrel
182
182
  ```
183
183
 
184
- Leave the top level's `"workers_dev": false`, `"preview_urls": false`, and `"triggers": { "crons": [] }` as they are, and never add a route there: they are what keep a deploy like that inert. Keep the empty `crons` rather than deleting the line, since only an empty list clears a schedule an earlier deploy left.
185
-
186
184
  ## Check it
187
185
 
188
- 1. The app answers on your hostname. A new hostname can take a few minutes to get its certificate.
186
+ 1. The app answers on your hostname. If `curl` reports a certificate error, Cloudflare is still issuing the new hostname's certificate; try again shortly.
189
187
 
190
188
  ```bash
191
189
  curl https://newsletter.example.com/health
@@ -53,8 +53,6 @@ Every Access application denies everyone until a policy allows them.
53
53
 
54
54
  Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in your `Publishers` policy should be one. To let someone sign in another way, such as a one-time code by email, add it under **Zero Trust → Integrations → Identity providers**.
55
55
 
56
- The app accepts only two kinds of policy on this application. One is an Allow policy on people's emails, like this one. The other is a Service Auth policy on a service token, from [Automate with a service token](../guides/02-automate.md). A login through any other kind, such as a certificate, a device check, or an identity provider that gives no email, is refused with a `401`, and the editor says so.
57
-
58
56
  ## 4. Give the app its two values
59
57
 
60
58
  The app verifies each login against your team domain, and checks that it was issued for this application.
@@ -107,6 +105,10 @@ The app verifies each login against your team domain, and checks that it was iss
107
105
  {"status":"ok","service":"kestrel"}
108
106
  ```
109
107
 
110
- If the editor says the configuration needs a fix, store the secret it names. The app needs both values, so while only one is stored, `/health` answers `500` naming the other, and the public pages say the newsletter is temporarily unavailable. If Cloudflare lets you in but the editor answers `401`, check the two secrets for a typo, or `ACCESS_ALLOWED_EMAILS` may leave out your address. If the editor says Access let you in but didn't say who you are, your login carried no email: sign out, and sign in through the `Publishers` policy.
108
+ If a check fails:
109
+
110
+ - **The editor says the configuration needs a fix.** Store the secret it names. While only one of the two is stored, `/health` answers `500` naming the other, and the public pages say the newsletter is temporarily unavailable.
111
+ - **Cloudflare lets you in, but the editor answers `401`.** Check the two secrets for a typo, and that `ACCESS_ALLOWED_EMAILS`, if you set it, includes your address.
112
+ - **The editor says Access let you in but didn't say who you are.** Your login carried no email, which happens through a kind of policy the app doesn't accept, such as a device check. Sign out, and sign in through the `Publishers` policy.
111
113
 
112
114
  Next, [connect Amazon SES](04-ses.md).
@@ -1,93 +1,66 @@
1
1
  # Connect Amazon SES
2
2
 
3
- Kestrel sends the newsletter's posts through Amazon SES, Amazon's email service. SES only delivers the mail: Kestrel keeps the list, each subscriber's consent, and the record of every send. SES prices every email alike, with no split between transactional and marketing mail, and costs the least at every list size above a tiny one. AWS reviews your account before it lets you send to anyone but yourself. This page asks for that early, and you test in the sandbox while you wait.
3
+ This page connects the app to Amazon Simple Email Service (Amazon SES), which delivers the newsletter's email. A new SES account starts in a sandbox, where it can send only to addresses you've verified, until AWS reviews it. This page requests that review early, so you can test in the sandbox while you wait.
4
4
 
5
- In this guide, you verify your sending hostname with SES and add a DMARC record. You forward readers' replies to the publisher's inbox, and ask AWS for production access. You also route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, set the provider, and deploy.
5
+ In this guide, you verify your sending hostname, set a custom MAIL FROM domain, add a DMARC record, and request production access. You then route bounces and complaints to the app, give it an AWS key, and deploy it with SES connected. Last, you forward readers' replies to the publisher's inbox.
6
6
 
7
7
  ## Before you begin
8
8
 
9
- - An [AWS account](https://portal.aws.amazon.com/billing/signup).
10
-
11
- To move an instance that already sends through another provider or another SES account, read [Switch an instance that already sends](#switch-an-instance-that-already-sends) first.
9
+ - An [AWS account](https://portal.aws.amazon.com/billing/signup), if you haven't created one already.
10
+ - The publisher's inbox, for readers' replies.
12
11
 
13
12
  ## 1. Verify your sending hostname
14
13
 
15
- SES sends only from an identity you've verified. Verifying the whole hostname, rather than one address, lets any address on it send. It also sets up DKIM, which lets inboxes trust your mail.
14
+ SES sends only from an identity you've verified. Verifying the whole hostname lets any address on it send, and sets up DKIM, which lets inboxes trust your mail.
16
15
 
17
- 1. Open the [SES console](https://console.aws.amazon.com/ses/), and choose the region you send from in the menu at the top right. Any region that offers SES works, so pick one near you. A few newer regions must be [enabled for your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-regions.html) before you can use them. SES keeps identities, quotas, and the sandbox separately in each region, so do every step in this one.
16
+ 1. Open the [SES console](https://console.aws.amazon.com/ses/), and choose a region near you in the menu at the top right. SES keeps identities, quotas, and the sandbox separately in each region, so do every step on this page in this one. A few newer regions must be [enabled for your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-regions.html) first.
18
17
 
19
- 1. Note the region's code. The menu shows a name, such as Europe (Ireland), but later steps need its code, such as `eu-west-1`. [Amazon SES endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) lists each region's code beside its name. The MAIL FROM record, the SNS access policy, and `AWS_REGION` all use this one code.
18
+ 1. Note the region's code, such as `eu-west-1`. The menu shows only its name, such as Europe (Ireland), and [Amazon SES endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) lists each code beside its name. Later steps use this code.
20
19
 
21
20
  1. Under **Configuration**, choose **Identities**, then **Create identity**.
22
21
 
23
- 1. Select **Domain**, and enter `send.example.com`. Keep the default DKIM settings: **Easy DKIM**, with a 2048-bit key. Choose **Create identity**.
22
+ 1. Select **Domain**, and enter `send.example.com`. Keep the default DKIM settings, **Easy DKIM** with a 2048-bit key, and choose **Create identity**.
24
23
 
25
24
  1. On the identity's **Authentication** tab, expand **Publish DNS records**. It lists three CNAME records.
26
25
 
27
- 1. In the Cloudflare dashboard, add each record to your domain's DNS records, exactly as SES lists it. Set each one's proxy status to **DNS only**. Cloudflare adds your domain to the name, so a name ends in `._domainkey.send`.
26
+ 1. In the Cloudflare dashboard, add each record to your domain's DNS records, exactly as SES lists it, with its proxy status set to **DNS only**. Cloudflare adds your domain to the name, so each name ends in `._domainkey.send`.
28
27
 
29
28
  1. Wait until SES shows **DKIM configuration** as **Successful** and **Identity status** as **Verified**.
30
29
 
31
- ## 2. Add a DMARC record
32
-
33
- DMARC tells inboxes what to do with mail that fails its checks, and sends you reports about it. SES doesn't add this record, so you add it yourself. It starts in monitor mode, which reports without blocking anything.
34
-
35
- 1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
36
-
37
- - **Type:** `TXT`
38
- - **Name:** `_dmarc.send`
39
- - **Content:** `v=DMARC1; p=none; rua=mailto:dmarc@example.com`
40
-
41
- 1. Replace `dmarc@example.com` with an address that receives mail. Reports arrive there as attachments.
42
-
43
- [Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
44
-
45
- ## 3. Receive replies at your From address
46
-
47
- When a reader replies to a post, or to their confirmation email, the reply goes to your From address, `newsletter@send.example.com`. Nothing receives mail there yet, so the reply bounces. Kestrel only sends mail, so you give the address an inbox yourself. Here you forward it to the publisher's inbox, with Cloudflare Email Routing, which is free on every plan. The publisher is whoever writes and sends the newsletter, which may be you.
30
+ ## 2. Set a custom MAIL FROM domain
48
31
 
49
- `send.example.com` receives no other mail, so turning on Email Routing there changes nothing else. Your SES records sit on other names under it. Inboxes check SES's mail for SPF at its return path, not here, so sending is unaffected.
50
-
51
- 1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
52
-
53
- 1. Add `send.example.com` to Email Routing. Cloudflare adds subdomains from your domain's own settings: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
54
-
55
- 1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
32
+ By default, SES sends with its own return path, so Gmail shows your mail as "mailed-by `amazonses.com`", and only DKIM ties it to your domain. A custom MAIL FROM domain puts your hostname there too, so SPF also passes for your domain. Your mail then still passes DMARC, the check section 3 sets up, if DKIM ever fails. It also meets Gmail's bulk-sender rule to set up SPF for your domain.
56
33
 
57
- 1. Under **Destination Addresses**, add the publisher's inbox. Open the email Cloudflare sends to it, and select **Verify email address**. If it isn't your inbox, ask the publisher to do this step. An address you've already verified with Cloudflare, such as one for [notifications](../guides/03-notifications.md), is listed already.
58
-
59
- 1. Under **Routing Rules**, select **Create routing rule**. For **Email pattern**, enter `newsletter`, the part of your From address before the `@`, and select `send.example.com`. Set **Action** to **Send to an email**, select the publisher's inbox as the **Destination**, and save.
60
-
61
- On a subdomain, Email Routing forwards only the addresses you add a rule for, with no catch-all. That's all a From address needs.
34
+ 1. In SES, open the `send.example.com` identity. Under **Custom MAIL FROM domain**, choose **Edit**.
62
35
 
63
- - **Automatic replies arrive too.** Out-of-office and other automatic replies go to your From address, so they reach the publisher's inbox. Bounces don't: they go to SES's return path, and the app handles them as before.
64
- - **Your DMARC reports need an inbox too.** Your DMARC record's report address may not receive mail. If so, change the record's `rua` to `mailto:dmarc@send.example.com`, and add a second rule that forwards `dmarc` the same way.
65
- - **Answers come from the publisher's inbox.** A forwarded reply comes from the reader's own address. An answer to it goes out from the inbox it was forwarded to, not from `newsletter@send.example.com`.
36
+ 1. Select **Use a custom MAIL FROM domain**, and enter `bounce.send.example.com`. For **Behavior on MX failure**, keep **Use default MAIL FROM domain**. Choose **Save changes**.
66
37
 
67
- To answer from the newsletter's own address, give it a real mailbox instead of forwarding it. Point `send.example.com`'s MX records at a mail host you use, such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034). Then create `newsletter@send.example.com` there, as the host's docs describe. If you send from it, the host's SPF and DKIM records go on `send.example.com` beside SES's. SES's mail is unaffected, since its SPF is checked at its return path. And if your From address is on a domain that already receives mail, replies already reach it, and you can skip this section.
38
+ 1. In Cloudflare, add the two records SES lists, with your region's code in place of `us-east-1`:
68
39
 
69
- Some replies ask the publisher to do something, and Kestrel can't see them, so the publisher acts on them, as [Sending rules](../publishing/02-sending-rules.md#when-a-reader-asks-to-leave-or-to-be-deleted) describes.
40
+ - **Type:** `MX`, **Name:** `bounce.send`, **Mail server:** `feedback-smtp.us-east-1.amazonses.com`, **Priority:** `10`
41
+ - **Type:** `TXT`, **Name:** `bounce.send`, **Content:** `v=spf1 include:amazonses.com ~all`
70
42
 
71
- ## 4. (Optional) Set a custom MAIL FROM domain
43
+ 1. Wait until SES shows the MAIL FROM domain's status as successful.
72
44
 
73
- SES sends with its own return path by default, so SPF passes for `amazonses.com` rather than for your hostname. DMARC still passes, through DKIM. A custom MAIL FROM domain makes SPF pass for your hostname too, a second check in your favor.
45
+ ## 3. Add a DMARC record
74
46
 
75
- 1. In SES, open the `send.example.com` identity. Under **Custom MAIL FROM domain**, choose **Edit**.
47
+ DMARC tells inboxes what to do with mail that fails its checks, and sends you reports about it. SES doesn't add this record, so you add it yourself. It starts in monitor mode, which reports without blocking anything.
76
48
 
77
- 1. Select **Use a custom MAIL FROM domain**, and enter `bounce.send.example.com`. For **Behavior on MX failure**, keep **Use default MAIL FROM domain**. Choose **Save changes**.
49
+ 1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
78
50
 
79
- 1. In Cloudflare, add the two records SES lists. Replace `us-east-1` with your region's code from section 1:
51
+ - **Type:** `TXT`
52
+ - **Name:** `_dmarc.send`
53
+ - **Content:** `v=DMARC1; p=none; rua=mailto:dmarc@example.com`
80
54
 
81
- - **Type:** `MX`, **Name:** `bounce.send`, **Mail server:** `feedback-smtp.us-east-1.amazonses.com`, **Priority:** `10`
82
- - **Type:** `TXT`, **Name:** `bounce.send`, **Content:** `v=spf1 include:amazonses.com ~all`
55
+ 1. Replace `dmarc@example.com` with an address that receives mail. Reports arrive there as attachments.
83
56
 
84
- 1. Wait until SES shows the MAIL FROM domain as **Successful**.
57
+ [Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
85
58
 
86
- ## 5. Request production access
59
+ ## 4. Request production access
87
60
 
88
- A new SES account starts in the sandbox. It sends only to addresses you've verified in SES, up to 200 emails a day and one a second. Ask AWS to lift those limits now, and test while you wait.
61
+ A new SES account starts in the sandbox: it sends only to addresses you've verified, up to 200 emails a day and one a second. Ask AWS to lift those limits now, and test while you wait.
89
62
 
90
- 1. Name the publication first, or ask the publisher for its name, since AWS may visit its website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline. Until then, the public pages show the name in your From address. The subscribe form stays closed until section 8 sets the provider.
63
+ 1. Name the publication, since AWS may visit its website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline, or ask the publisher for them.
91
64
 
92
65
  1. In SES, open the **Account dashboard**, select **View Get set up page**, and then **Request production access**.
93
66
 
@@ -96,32 +69,28 @@ A new SES account starts in the sandbox. It sends only to addresses you've verif
96
69
  - **Mail type:** Marketing
97
70
  - **Website URL:** `https://newsletter.example.com/`
98
71
 
72
+ Leave the other fields as they are.
73
+
99
74
  1. Check the acknowledgement, and select **Submit request**. AWS Support answers by email, and may ask how you collect addresses and handle bounces.
100
75
 
101
- 1. While you wait, verify the two addresses you test with in [Verify it works](05-verify.md#before-you-begin), so the sandbox lets you mail them. For each, under **Identities**, choose **Create identity**, select **Email address**, and follow the link in the email AWS sends.
76
+ 1. Verify two addresses you own, such as a personal address and a work address, so the sandbox lets you mail them. Your test emails go to these. For each, under **Identities**, choose **Create identity**, select **Email address**, and follow the link in the email AWS sends.
102
77
 
103
- If AWS asks for more detail, reply in the support case. If it denies the request, reply there, or open a new request from the **Account dashboard**. Either way, say how Kestrel handles the list:
78
+ If AWS asks for more detail, or denies the request, reply in the support case, or open a new request from the **Account dashboard**. Say how Kestrel handles the list:
104
79
 
105
80
  - Signups use double opt-in: each subscriber confirms by email before joining the list.
106
81
  - Every post offers one-click unsubscribe.
107
- - Bounces and complaints reach the app through SNS, and it stops mailing those addresses automatically.
82
+ - Bounces and complaints reach the app, and it stops mailing those addresses automatically.
108
83
  - How many subscribers the newsletter expects, and how often it sends.
109
84
 
110
- Your account stays in the sandbox until AWS grants access, so don't go live before then. In the sandbox, SES refuses mail to any address you haven't verified:
85
+ In the meantime, continue with the rest of this guide.
111
86
 
112
- - A signup from an unverified address gets no confirmation email. The app emails the publisher about it, at most once a day.
113
- - A post goes to your verified addresses and then waits, with the line "The provider is refusing the account" and SES's words. No reader is marked unsent. Once AWS grants access, the post goes to the rest at its next retry, mailing no one twice.
114
- - A daily quota refusal in the sandbox (200 emails a day) means the same thing: request production access rather than a higher quota.
87
+ ## 5. Send bounces and complaints to the app
115
88
 
116
- When AWS grants access, the **Account dashboard** shows your daily quota and your maximum send rate. You set the rate in the app in section 8. The rate decides how fast a send goes out, and [As your list grows](../guides/09-as-your-list-grows.md) says when to ask AWS to raise it.
117
-
118
- ## 6. Send bounces and complaints to SNS
119
-
120
- SES reports what happens to each email through SNS, the AWS notification service. A configuration set says which events to report, and an SNS topic carries them to the app.
89
+ SES reports bounces and complaints through Amazon Simple Notification Service (SNS), which delivers them to the app. The app then stops mailing an address that bounced or complained. A configuration set says which events SES reports, and an SNS topic carries them.
121
90
 
122
91
  1. In SES, under **Configuration**, choose **Configuration sets**, then **Create set**. Name it `kestrel-mail`, and choose **Create set**.
123
92
 
124
- 1. On the set's **Event destinations** tab, choose **Add destination**. Select **Hard bounces**, **Complaints**, **Deliveries**, and **Rendering failures**, and choose **Next**. A rendering failure is an email SES took but couldn't build. It shouldn't happen, and the app logs it as an error if it does.
93
+ 1. On the set's **Event destinations** tab, choose **Add destination**. Select **Hard bounces**, **Complaints**, **Deliveries**, and **Rendering failures**, and choose **Next**. The app logs a rendering failure, an email SES couldn't build, as an error.
125
94
 
126
95
  1. For **Destination type**, select **Amazon SNS**. Name it `kestrel-sns`, and keep **Event publishing** enabled. Choose **Next**.
127
96
 
@@ -129,7 +98,7 @@ SES reports what happens to each email through SNS, the AWS notification service
129
98
 
130
99
  1. Make the set your hostname's default, so SES reports on every email sent from it. Open the `send.example.com` identity, and choose the **Configuration set** tab. Under **Default configuration set**, choose **Edit**, select `kestrel-mail`, and save.
131
100
 
132
- 1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), and copy its ARN. It looks like `arn:aws:sns:us-east-1:111122223333:kestrel-events`. After `sns` comes your region's code, then your 12-digit account ID. The account menu at the top right of the console also shows the account ID.
101
+ 1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), and find its ARN. It looks like `arn:aws:sns:us-east-1:111122223333:kestrel-events`, where `us-east-1` is your region's code and `111122223333` is your account ID.
133
102
 
134
103
  1. Let SES publish to the topic. Choose **Edit**, expand **Access policy**, and add this statement to the `Statement` list:
135
104
 
@@ -148,13 +117,23 @@ SES reports what happens to each email through SNS, the AWS notification service
148
117
  }
149
118
  ```
150
119
 
151
- Replace the topic ARN, and both copies of the account ID, with the values from your ARN. Replace `us-east-1` with your region's code, the same one the ARN shows. Choose **Save changes**.
120
+ Replace the topic ARN, both copies of the account ID, and `us-east-1` with the values from your ARN. Choose **Save changes**.
121
+
122
+ 1. Store the topic's ARN in the app. In your instance's repository, run this, and paste the ARN at the prompt. The app accepts events only from this topic:
123
+
124
+ ```bash
125
+ npx wrangler secret put SNS_TOPIC_ARN --env production
126
+ ```
152
127
 
153
- You subscribe the app to the topic in section 9, once it runs on SES.
128
+ 1. Store the configuration set's name, `kestrel-mail`. It's optional but recommended: the app names the set on every email, so SES reports on each one even without the hostname's default. Type it exactly as the set is named in this region, since SES refuses every email that names a set it can't find:
154
129
 
155
- ## 7. Create the app's AWS key
130
+ ```bash
131
+ npx wrangler secret put SES_CONFIGURATION_SET --env production
132
+ ```
156
133
 
157
- The app sends through an IAM user whose key can only send email. That limits the harm if the key ever leaks.
134
+ ## 6. Create the app's AWS key
135
+
136
+ The app sends through an AWS Identity and Access Management (IAM) user whose key can only send email. That limits the harm if the key ever leaks.
158
137
 
159
138
  1. In the [IAM console](https://console.aws.amazon.com/iam/), go to **Users**, and choose **Create user**. Name it `kestrel-sender`, and leave console access off.
160
139
 
@@ -183,13 +162,7 @@ The app sends through an IAM user whose key can only send email. That limits the
183
162
 
184
163
  1. Open the user, and on the **Security credentials** tab, choose **Create access key**. For the use case, choose **Application running outside AWS**.
185
164
 
186
- 1. Copy the access key ID and the secret access key. AWS shows the secret only once.
187
-
188
- ## 8. Set the provider and deploy
189
-
190
- Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` connects SES. Store the secrets before you set it. With `PROVIDER` set to `ses` and a required secret missing, the app refuses to run, and sends nothing, until you store it. `/health` and the editor's **A setting needs fixing** page name the missing secret, and reader pages say the newsletter is temporarily unavailable. `SES_CONFIGURATION_SET` is the one optional secret.
191
-
192
- 1. Store the access key ID, and paste it at the prompt:
165
+ 1. Keep the page open: AWS shows the secret access key only once. Store the access key ID in the app, and paste it at the prompt:
193
166
 
194
167
  ```bash
195
168
  npx wrangler secret put AWS_ACCESS_KEY_ID --env production
@@ -201,17 +174,9 @@ Until now the app has run with no email provider connected, refusing to send. Se
201
174
  npx wrangler secret put AWS_SECRET_ACCESS_KEY --env production
202
175
  ```
203
176
 
204
- 1. Store the topic ARN from section 6. The app accepts events only from this topic:
205
-
206
- ```bash
207
- npx wrangler secret put SNS_TOPIC_ARN --env production
208
- ```
209
-
210
- 1. Store the configuration set's name, `kestrel-mail`. It's optional but recommended: the app names it on every email, so SES reports on each one even without the default. Type it exactly as the set is named in this region: if SES can't find the set, it refuses every email, and a list send is held until the name is fixed or the set created:
177
+ ## 7. Set the provider and deploy
211
178
 
212
- ```bash
213
- npx wrangler secret put SES_CONFIGURATION_SET --env production
214
- ```
179
+ Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` to `ses` connects SES, with the secrets you stored in sections 5 and 6. If a required one is missing, the app refuses to run until you store it, and `/health` names it.
215
180
 
216
181
  1. In `wrangler.jsonc`, edit the `production` block's `vars`. Change `PROVIDER` to `ses` (it's `""` on a new instance; add it if your block has none), and add `SES_MAX_SEND_RATE`. Change `AWS_REGION` to your region's code from section 1. Your other values stay as they are. The block ends up like this:
217
182
 
@@ -244,7 +209,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
244
209
  npx wrangler deploy --env production
245
210
  ```
246
211
 
247
- ## 9. Subscribe the app to the topic
212
+ ## 8. Subscribe the app to the topic
248
213
 
249
214
  SNS delivers events only to a subscriber that has confirmed. The app confirms on its own, once it checks that the request came from your topic.
250
215
 
@@ -260,7 +225,9 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
260
225
 
261
226
  1. Reload the subscription. Its status changes to **Confirmed**, with nothing for you to click.
262
227
 
263
- 1. Give SNS longer to retry. By default it retries a failed delivery 3 times over about a minute, then discards the event, so a bounce that arrives while the app is briefly down or mid-deploy is lost, and the address gets your next post. Open the topic, choose **Edit**, and expand **Delivery policy (HTTP/S)**. Clear **Use the default delivery policy**, set these, and choose **Save changes**:
228
+ If it stays **Pending confirmation**, check that your Access application covers only `/dashboard` and `/api`, that `SNS_TOPIC_ARN` matches the topic's ARN, and that section 7's deploy finished. A mismatched ARN shows in the app's logs as a `webhook.refused` error naming both. Then request confirmation again from the topic's subscriptions. SNS doesn't let you delete a pending subscription, and removes it on its own after 48 hours.
229
+
230
+ 1. Give SNS longer to retry. By default it retries a failed delivery 3 times over about a minute, then discards the event, so a bounce that arrives mid-deploy is lost. Open the topic, choose **Edit**, and expand **Delivery retry policy (HTTP/S)**. Turn off the default policy, set these values, and choose **Save changes**:
264
231
 
265
232
  | Field | Value |
266
233
  | --- | --- |
@@ -272,25 +239,35 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
272
239
  | Maximum delay retries | `6` |
273
240
  | Retry-backoff function | Exponential |
274
241
 
275
- SNS then keeps trying for most of an hour, the longest it allows. It never retries an event the app refused. The app refuses only an event it can't verify came from your topic, and logs an error when one claims to be from SNS.
242
+ SNS then keeps trying for about 40 minutes. It allows an hour at most.
243
+
244
+ 1. **(Optional)** Keep what SNS still can't deliver. On the subscription, choose **Edit**, and set a **Redrive policy (dead-letter queue)** to an SQS queue of your own. SNS then moves an event there instead of discarding it. [Amazon SNS dead-letter queues](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html) covers the queue and its permissions.
276
245
 
277
- 1. **(Optional)** Keep what SNS still can't deliver. On the subscription, choose **Edit**, and set a **Redrive policy (dead-letter queue)** to an SQS queue of your own. SNS then moves an event there instead of discarding it, where you can read it later. [Amazon SNS dead-letter queues](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html) covers the queue and its permissions.
246
+ ## 9. Receive replies at your From address
278
247
 
279
- If it stays **Pending confirmation**, the request never reached the app, or the app refused it. Check, in this order:
248
+ Replies to a post go to your From address, `newsletter@send.example.com`, which has no inbox yet, so they bounce. This section forwards that address to the publisher's inbox with Cloudflare Email Routing. Forwarding only receives mail, so it doesn't change how SES sends.
280
249
 
281
- - **Access covers `/webhooks/ses`.** If your Access application covers more than `/dashboard` and `/api`, SNS gets the login page instead of the app. Remove that path from your Access application, so only `dashboard` and `api` remain.
282
- - **`SNS_TOPIC_ARN` matches the topic's ARN.** If it doesn't, the app's logs show a `webhook.refused` error naming both.
283
- - **The deploy in section 8 finished.**
250
+ 1. Check that your From address has no inbox yet. This prints nothing:
284
251
 
285
- Once you've fixed the cause, select the subscription, and choose **Request confirmation**. SNS sends the request again, and the app confirms it. SNS doesn't let you delete a subscription while it's pending, and removes it on its own after 48 hours.
252
+ ```bash
253
+ dig +short MX send.example.com
254
+ ```
286
255
 
287
- ## Switch an instance that already sends
256
+ If it lists mail servers, `send.example.com` already receives mail elsewhere, and Email Routing would replace those servers. Check that your mail host has a mailbox for `newsletter@send.example.com`, and skip to [Check it](#check-it).
288
257
 
289
- Change `PROVIDER`, or move to another account with the same provider, only while no send is sending. The dashboard lists any send in progress.
258
+ 1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
290
259
 
291
- When a request to a provider gets no answer, Kestrel can't know whether its recipients were mailed. Moving to SES, from another provider or another SES account, mails none of them twice. Kestrel never repeats an unanswered request to SES, so those recipients wait for you to decide whether they were mailed. Use **Resolve** on the send's page.
260
+ 1. Add `send.example.com` to Email Routing: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
292
261
 
293
- Kestrel can't tell whether those emails went out. SES's console shows sent messages only if you've turned on Virtual Deliverability Manager, which this guide doesn't set up. So **Assume not sent** is the safe default. Those readers may miss this post, but they get your next one. No one is mailed twice. Choose **Assume sent** only when you know otherwise, such as a reader who says they got it.
262
+ 1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
263
+
264
+ 1. Under **Destination Addresses**, add the publisher's inbox. Open the email Cloudflare sends to it, and select **Verify email address**. If it isn't your inbox, ask the publisher to do this step.
265
+
266
+ 1. Under **Routing Rules**, select **Create routing rule**. For **Email pattern**, enter `newsletter`, the part of your From address before the `@`, and select `send.example.com`. Set **Action** to **Send to an email**, select the publisher's inbox as the **Destination**, and save.
267
+
268
+ 1. **(Optional)** Give your DMARC reports an inbox too. If the address in your DMARC record doesn't receive mail, add a rule that forwards `dmarc` the same way. Then change the record's `rua` to `mailto:dmarc@send.example.com`.
269
+
270
+ Out-of-office and other automatic replies reach the publisher's inbox too. Bounces don't: they go to SES, which reports them to the app. The publisher answers from their own inbox, not from `newsletter@send.example.com`. To answer from the newsletter's address instead, give it a mailbox at a mail host such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034), in place of forwarding.
294
271
 
295
272
  ## Check it
296
273
 
@@ -318,26 +295,16 @@ Kestrel can't tell whether those emails went out. SES's console shows sent messa
318
295
 
319
296
  1. The SNS subscription shows **Confirmed**.
320
297
 
321
- 1. Your From address has somewhere to go. In Cloudflare, your domain's DNS records show the ones Email Routing added for `send`, beside section 1's DKIM records:
322
-
323
- - **Type:** `MX`, **Name:** `send`, three of them, for `route1.mx.cloudflare.net`, `route2.mx.cloudflare.net`, and `route3.mx.cloudflare.net`
324
- - **Type:** `TXT`, **Name:** `send`, **Content:** `v=spf1 include:_spf.mx.cloudflare.net ~all`
325
- - **Type:** `TXT`, **Name:** `cf2024-1._domainkey.send`, Cloudflare's DKIM key
326
-
327
- The world sees the MX records too:
298
+ 1. Your From address receives mail. This lists Cloudflare's three `route` mail servers, or your own mail host's if you skipped section 9's forwarding:
328
299
 
329
300
  ```bash
330
301
  dig +short MX send.example.com
331
302
  ```
332
303
 
333
- It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows the publisher's inbox as verified. A reply to your test email in the next step proves the forwarding works.
334
-
335
304
  1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before.
336
305
 
337
306
  1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
338
307
 
339
- The test sends in the next step prove the rest. In the sandbox, send them only to the addresses you verified in section 5, and to the SES test addresses that page lists.
340
-
341
- SES charges for each email it sends, test emails included. [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists the rates.
308
+ The test emails in the next step prove the rest, including a reply. In the sandbox, they go only to the addresses you verified in section 4, and to the SES test addresses that page lists. SES charges for each email it sends, test emails included, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
342
309
 
343
310
  Next, [verify it works](05-verify.md).
@@ -1,12 +1,12 @@
1
1
  # Verify it works
2
2
 
3
- Some things only show up with real email: whether inboxes trust your mail, how it looks in a real mail client, and whether bounces and unsubscribes come back to the app. Check them now, while the list is empty and every email goes only to you.
3
+ This page checks your instance end to end with real email, while the list is empty and every email goes only to you. You confirm that inboxes trust your mail, that posts look right in a mail client, and that bounces, complaints, and unsubscribes reach the app.
4
4
 
5
5
  In this guide, you send yourself a test email, reply to it, and check that it passes authentication. Then you set up notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, read the app's logs, check that SES can send to anyone, and clean up the test subscribers.
6
6
 
7
7
  ## Before you begin
8
8
 
9
- - Two email addresses you own, such as a personal address and a work address. Some steps use both. While your SES account is in the sandbox, verify both in SES first, as [Connect Amazon SES](04-ses.md#5-request-production-access) describes.
9
+ - The two addresses you verified in SES in [Connect Amazon SES](04-ses.md#4-request-production-access). Some steps use both.
10
10
  - A Gmail address, or another mail client that shows a message's original source and an **Unsubscribe** button.
11
11
 
12
12
  ## 1. Send yourself a test email
@@ -17,9 +17,9 @@ A test email goes through the same code as a real send. If the test looks right,
17
17
 
18
18
  1. Select **Send test email**, enter your address, and send it.
19
19
 
20
- 1. Open the email. It should arrive in your inbox, not in spam. The image loads and the subject reads as you wrote it. Its **View in browser** link opens "Nothing here yet" until the post goes out, and its unsubscribe link opens the page a subscriber sees, marked as a test and showing a sample address. Its button walks through to the confirmation, but a test email unsubscribes no one. Section 5 unsubscribes for real.
20
+ 1. Open the email. It arrives in your inbox, not in spam, the image loads, and the subject reads as you wrote it. Its links work, but a test email unsubscribes no one; section 5 unsubscribes for real.
21
21
 
22
- 1. Reply to it, as a reader would. Reply from an address other than the inbox your From address forwards to, such as your second address. Gmail doesn't show you a message you sent when it comes back to you. Send that address a test email too if you need one. The reply arrives in the inbox you chose in [Connect Amazon SES](04-ses.md#3-receive-replies-at-your-from-address). The app can't see your forwarding, so this is the only proof that readers can reach the publisher. If the reply bounces, check the routing rule and the destination address there.
22
+ 1. Send a test email to your second address, not the publisher's inbox, and reply to it from there, as a reader would. The reply arrives in the publisher's inbox, the one you chose in [Connect Amazon SES](04-ses.md#9-receive-replies-at-your-from-address), or in your From address's own mailbox if you skipped forwarding. If it bounces, check the routing rule and the destination address there.
23
23
 
24
24
  ## 2. Check that it passes authentication
25
25
 
@@ -33,13 +33,13 @@ Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing
33
33
  - `dmarc=pass` for `send.example.com`
34
34
  - `spf=pass`, for a hostname under `send.example.com`
35
35
 
36
- With no custom MAIL FROM domain, SPF passes for `amazonses.com` instead. That's expected, and DMARC still passes.
36
+ If SPF passes for `amazonses.com` instead, SES couldn't read your custom MAIL FROM domain's MX record, and used its own. DMARC still passes, but check the records from [Connect Amazon SES](04-ses.md#2-set-a-custom-mail-from-domain).
37
37
 
38
38
  If a check fails, compare your DNS records with the ones in [Connect Amazon SES](04-ses.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
39
39
 
40
40
  ## 3. Set up notifications
41
41
 
42
- Kestrel emails the notifications address when a send finishes, and right away when one runs into a problem. Notifications go through your email provider. To send them through Cloudflare instead, see [Notifications through Cloudflare's email](../guides/03-notifications.md).
42
+ Kestrel emails the notifications address when a send finishes, and right away when one runs into a problem.
43
43
 
44
44
  1. In the editor, open **Settings → Notifications**.
45
45
 
@@ -55,18 +55,16 @@ This is a real send, to a list of one. It proves the whole path: subscribing, co
55
55
 
56
56
  1. Open `https://newsletter.example.com/subscribe`, and subscribe with your second address.
57
57
 
58
- 1. Open the confirmation email, follow its link, and select **Confirm**. In the editor, **Subscribers** shows the address as **Confirmed**.
58
+ 1. Open the confirmation email, follow its link, and select **Confirm subscription**. In the editor, **Subscribers** shows the address as **Confirmed**.
59
59
 
60
- 1. Open the post, select **Schedule**, and then **Send now**. Every send waits at least five minutes before it goes out, so you can cancel a mistake.
60
+ 1. Open the post, select **Schedule**, and then **Send now**.
61
61
 
62
- 1. Uncheck **Include in archive**. The post still goes to subscribers, but it's sent as email only: it isn't in the public archive or on the home page. That way the test proves the send without becoming the publication's first archived post, and the dashboard keeps its **Finish setting up** steps for the publisher.
62
+ 1. In the **Send now?** dialog, uncheck **Include in archive**, and select **Send now**. The post goes out as email only, so it doesn't become the publication's first archived post. The dashboard keeps its **Finish setting up** steps for the publisher. Every send waits at least five minutes before it goes out, so you can cancel a mistake.
63
63
 
64
- 1. Cancel the send, to see that canceling stops it. Then select **Schedule** and **Send now** again, check that **Include in archive** is still unchecked, and let it go.
64
+ 1. Cancel the send, to see that canceling stops it. Then send it again the same way, with **Include in archive** unchecked, and let it go.
65
65
 
66
66
  1. When the send finishes, the post arrives at your second address. A notification arrives at your first, with the subject `Sent:` and the post's title.
67
67
 
68
- If you verified this instance before Email only existed, the test post is in the archive. Open it under **Sent**, and select **Remove from archive…**.
69
-
70
68
  ## 5. Unsubscribe
71
69
 
72
70
  Every email carries an unsubscribe link, and a header that lets mail clients show their own **Unsubscribe** button. Both work in one step, with no login.
@@ -88,7 +86,7 @@ When an address bounces or marks your mail as spam, SES tells the app through SN
88
86
 
89
87
  1. In the editor, **Subscribers** lists the bounce address as **Pending** with a **bounced** flag, and the complaint address as **Unsubscribed**. Open its row: its page says it left by a complaint. The app mails neither again.
90
88
 
91
- If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set and the SNS subscription in [Connect Amazon SES](04-ses.md#6-send-bounces-and-complaints-to-sns).
89
+ If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set in [Send bounces and complaints to the app](04-ses.md#5-send-bounces-and-complaints-to-the-app), and the subscription in [Subscribe the app to the topic](04-ses.md#8-subscribe-the-app-to-the-topic).
92
90
 
93
91
  ## 7. Read the logs
94
92
 
@@ -100,21 +98,19 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
100
98
 
101
99
  1. Filter on `event` equal to `send.completed`. Your send from section 4 is there.
102
100
 
103
- 1. Filter on `event` equal to `send.scheduled`. Your two sends from section 4 are there, each with `act` equal to `send_now`, `principal` equal to `human`, and `client` equal to `editor`, and `send.canceled` shows the one you canceled. Each line that records an action, such as `send.canceled` or `import.committed`, says who took it the same way: `principal` is `human` for a person or `service` for a service token, and `client` names the program a person used. A service token's lines have no `client`, and neither does a person's through a program that doesn't name itself.
104
-
105
- 1. Filter on `principal` equal to `service` to see what automation changed. On a new instance it finds nothing. Filter on `client` equal to `claude` or `claude-code` to see what Claude changed.
101
+ 1. Filter on `event` equal to `send.scheduled`. Your two sends from section 4 are there, and `send.canceled` shows the one you canceled. Each line that records an action says who took it. Its `principal` is `human` or `service`, and its `client` names the program a person used, such as `editor`.
106
102
 
107
103
  1. Filter on `level` equal to `error`. A healthy instance shows nothing.
108
104
 
109
- If you search a long time range, Cloudflare may show only a sample of the matching lines. To follow one send, narrow the range to a few hours and filter on its `sendId`. On the Free plan, Cloudflare stops keeping logs for the rest of the day once the day's allowance is used up, so a very large send can leave the rest of that day unlogged. [Cloudflare's pricing page](https://developers.cloudflare.com/observability/pricing/) has the current limits and how long each plan keeps logs.
105
+ To follow one send, narrow the time range and filter on its `sendId`, since a long range may show only a sample. On the Free plan, Cloudflare stops keeping logs for the rest of the day once its daily allowance is used, as [its pricing page](https://developers.cloudflare.com/observability/pricing/) describes.
110
106
 
111
107
  ## 8. Check that SES can send to anyone
112
108
 
113
- [Connect Amazon SES](04-ses.md#5-request-production-access) asked AWS for production access. Until AWS grants it, the sandbox mails only your verified addresses, so check it before you go live.
109
+ [Connect Amazon SES](04-ses.md#4-request-production-access) asked AWS for production access. Until AWS grants it, the sandbox mails only your verified addresses, so check it before you go live.
114
110
 
115
111
  1. In SES, open the **Account dashboard** in the region the app sends from. It no longer says your account is in the sandbox, and it shows your daily quota and maximum send rate.
116
112
 
117
- 1. Check that `SES_MAX_SEND_RATE` in `wrangler.jsonc` matches that rate, and that you deployed after setting it, as [Connect Amazon SES](04-ses.md#8-set-the-provider-and-deploy) describes.
113
+ 1. Check that `SES_MAX_SEND_RATE` in `wrangler.jsonc` matches that rate, and that you deployed after setting it, as [Connect Amazon SES](04-ses.md#7-set-the-provider-and-deploy) describes.
118
114
 
119
115
  If the dashboard still says sandbox, AWS hasn't answered yet. Every other section works in the sandbox, so come back to this one when it does.
120
116
 
@@ -17,7 +17,7 @@ You need:
17
17
 
18
18
  Managed OAuth lets an app such as Claude act with a person's account, the way the editor does in their browser. Cloudflare marks it as beta, and its [Managed OAuth](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/) page describes it.
19
19
 
20
- 1. In **Zero Trust**, go to **Access controls → Applications**, and select **Edit** on your application.
20
+ 1. In **Zero Trust**, go to **Access controls → Applications**, and select **Configure** on your application.
21
21
 
22
22
  1. On the **Additional settings** tab, select the **OAuth** filter, and find **Managed OAuth (Beta)**.
23
23
 
@@ -21,7 +21,7 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
21
21
 
22
22
  1. Cloudflare adds MX, SPF, and DKIM records for `newsletter.example.com`, and locks them. Wait until they show as added.
23
23
 
24
- Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/04-ses.md#3-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
24
+ Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/04-ses.md#9-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
25
25
 
26
26
  ## 2. Verify the notifications address
27
27
 
@@ -74,7 +74,7 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
74
74
 
75
75
  ## Check it
76
76
 
77
- 1. In the editor, open **Settings → Notifications**. **Sent through** reads "Cloudflare Email, separate from your newsletter's provider", and **From address** shows `kestrel@newsletter.example.com`.
77
+ 1. In the editor, open **Settings → Notifications**. **Sent through** reads "Cloudflare Email, separate from your newsletter's provider", and **From address** shows `Kestrel <kestrel@newsletter.example.com>`.
78
78
 
79
79
  1. Select **Send a test notification**. It arrives at the notifications address, and **Last notification** shows it as delivered.
80
80
 
@@ -13,10 +13,10 @@ This page covers what Kestrel handles, what you handle, and how to act on a read
13
13
 
14
14
  ## What you handle
15
15
 
16
- - **An honest From name and subject line.** The From name should say who the email is from, and the subject should match what's inside. The From name and address are set when Kestrel is deployed ([Connect Amazon SES](../get-started/04-ses.md#8-set-the-provider-and-deploy)).
16
+ - **An honest From name and subject line.** The From name should say who the email is from, and the subject should match what's inside. The From name and address are set when Kestrel is deployed ([Deploy the app](../get-started/02-deploy.md#4-configure-production)).
17
17
  - **Marking ads as ads.** When an email is an advertisement, say so clearly. In a newsletter that's mostly writing, labeling a sponsor's section is enough.
18
18
  - **A mailing address, when an email is commercial.** Most newsletters don't need one. [When to provide a mailing address](03-name-your-publication.md#when-to-provide-a-mailing-address) says when you do, beside where you add one.
19
- - **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, as [Connect Amazon SES](../get-started/04-ses.md#3-receive-replies-at-your-from-address) sets up. Kestrel never sees a reply, so carrying out what a reader asks is up to you.
19
+ - **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, as [Connect Amazon SES](../get-started/04-ses.md#9-receive-replies-at-your-from-address) sets up. Kestrel never sees a reply, so carrying out what a reader asks is up to you.
20
20
 
21
21
  ## When a reader asks to leave or to be deleted
22
22
 
@@ -16,5 +16,5 @@ This is general information, not legal advice. The FTC's [CAN-SPAM compliance gu
16
16
 
17
17
  ## Who sets the From name and address
18
18
 
19
- The From name and address your emails come from are set when Kestrel is deployed, so **Settings → Email sender** shows them read-only. To change them, follow [Connect Amazon SES](../get-started/04-ses.md#8-set-the-provider-and-deploy).
19
+ The From name and address your emails come from are set when Kestrel is deployed, so **Settings → Email sender** shows them read-only. To change them, follow [Deploy the app](../get-started/02-deploy.md#4-configure-production).
20
20