@kurtbruns/kestrel 1.16.0 → 1.18.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.
Files changed (37) hide show
  1. package/.dev.vars.example +10 -0
  2. package/CHANGELOG.md +38 -1
  3. package/bin/kestrel.mjs +1 -1
  4. package/cli/dev.mjs +3 -3
  5. package/cli/init.mjs +98 -4
  6. package/dist/public/dashboard/app.js +288 -286
  7. package/dist/public/dashboard/app.js.map +4 -4
  8. package/dist/public/dashboard/index.html +6 -3
  9. package/dist/public/dashboard/styles.css +10 -0
  10. package/dist/worker/index.js +5607 -5096
  11. package/docs/README.md +5 -3
  12. package/docs/get-started/02-deploy.md +25 -23
  13. package/docs/get-started/03-access.md +13 -7
  14. package/docs/get-started/04-aws-account.md +68 -0
  15. package/docs/get-started/05-ses.md +307 -0
  16. package/docs/get-started/06-production-access.md +64 -0
  17. package/docs/get-started/07-verify.md +121 -0
  18. package/docs/get-started/{06-go-live.md → 08-go-live.md} +2 -2
  19. package/docs/guides/01-allow-claude.md +2 -2
  20. package/docs/guides/02-automate.md +1 -1
  21. package/docs/guides/03-notifications.md +4 -4
  22. package/docs/guides/07-upgrade.md +4 -4
  23. package/docs/guides/09-as-your-list-grows.md +8 -18
  24. package/docs/publishing/02-sending-rules.md +3 -3
  25. package/docs/publishing/03-name-your-publication.md +3 -1
  26. package/docs/publishing/04-write-a-post.md +1 -1
  27. package/docs/publishing/06-troubleshooting.md +33 -68
  28. package/docs/publishing/09-share-your-subscribe-link.md +1 -1
  29. package/docs/publishing/10-check-your-setup.md +4 -2
  30. package/docs/reference/01-configuration.md +17 -13
  31. package/docs/reference/02-sending-domain-dns.md +5 -5
  32. package/migrations/0015_provider_account.sql +18 -0
  33. package/package.json +1 -1
  34. package/template/claude/CLAUDE.md +3 -3
  35. package/template/wrangler.jsonc +1 -1
  36. package/docs/get-started/04-ses.md +0 -343
  37. package/docs/get-started/05-verify.md +0 -135
@@ -0,0 +1,121 @@
1
+ # Verify it works
2
+
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
+
5
+ You can do all of this while SES is still in the sandbox, since every email goes to the addresses you verified in [Connect Amazon SES](05-ses.md#3-verify-your-test-addresses) or to SES's test addresses.
6
+
7
+ In this guide, you send yourself a test email, reply to it, and check that it passes authentication. You check that notifications arrive, 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, and clean up the test subscribers.
8
+
9
+ ## Before you begin
10
+
11
+ - The two addresses you verified in SES in [Connect Amazon SES](05-ses.md#3-verify-your-test-addresses). Some steps use both.
12
+ - A Gmail address, or another mail client that shows a message's original source and an **Unsubscribe** button.
13
+
14
+ ## 1. Send yourself a test email
15
+
16
+ A test email goes through the same code as a real send. If the test looks right, the real send looks the same.
17
+
18
+ 1. In the editor at `https://newsletter.example.com/dashboard/`, create a post with a subject, a short body, and an image.
19
+
20
+ 1. Select **Send test email**, enter your address, and send it.
21
+
22
+ 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.
23
+
24
+ 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](05-ses.md#8-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.
25
+
26
+ ## 2. Check that it passes authentication
27
+
28
+ Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing both is the best single sign that your mail reaches the inbox.
29
+
30
+ 1. Open the test email's original source. In Gmail, open the message's menu and select **Show original**.
31
+
32
+ 1. Find the `Authentication-Results` header. It shows:
33
+
34
+ - `dkim=pass` for `send.example.com`
35
+ - `dmarc=pass` for `send.example.com`
36
+ - `spf=pass`, for a hostname under `send.example.com`
37
+
38
+ 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 MAIL FROM records from [Connect Amazon SES](05-ses.md#1-verify-your-sending-hostname).
39
+
40
+ If a check fails, compare your DNS records with the ones in [Connect Amazon SES](05-ses.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
41
+
42
+ ## 3. Check that notifications arrive
43
+
44
+ Kestrel emails a notifications address when a send finishes, and right away when one runs into a problem. Check that they reach you, using one of the addresses you verified in SES.
45
+
46
+ 1. In the editor, open **Settings → Notifications**, enter that address, and select **Save**.
47
+
48
+ 1. Select **Send a test notification**. It arrives at that address.
49
+
50
+ The publisher sets their own address in [Check your setup](../publishing/10-check-your-setup.md).
51
+
52
+ ## 4. Send a real post to yourself
53
+
54
+ This is a real send, to a list of one. It proves the whole path: subscribing, confirming, scheduling, and sending.
55
+
56
+ 1. Open `https://newsletter.example.com/subscribe`, and subscribe with your second address.
57
+
58
+ 1. Open the confirmation email, follow its link, and select **Confirm subscription**. In the editor, **Subscribers** shows the address as **Confirmed**.
59
+
60
+ 1. Open the post, select **Schedule**, and then **Send now**.
61
+
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
+
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
+
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
+
68
+ ## 5. Unsubscribe
69
+
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.
71
+
72
+ 1. Open the post from section 4 in your mail client. It shows an **Unsubscribe** button next to the sender.
73
+
74
+ 1. Select it. In the editor, **Subscribers** shows the address as **Unsubscribed**, and later sends leave it out.
75
+
76
+ ## 6. Check that bounces and complaints come back
77
+
78
+ When an address bounces or marks your mail as spam, SES tells the app through SNS. The app then stops mailing that address: a bounce suppresses it, and a complaint unsubscribes it. SES has test addresses that act out each case without reaching a real person, and the sandbox lets you mail them.
79
+
80
+ 1. Open `https://newsletter.example.com/subscribe`, and subscribe each test address:
81
+
82
+ - `bounce@simulator.amazonses.com`, which bounces
83
+ - `complaint@simulator.amazonses.com`, which marks your mail as spam
84
+
85
+ Each one gets a confirmation email, which bounces or is marked as spam.
86
+
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.
88
+
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](05-ses.md#4-send-bounces-and-complaints-to-the-app), and the subscription in [Subscribe the app to the topic](05-ses.md#7-subscribe-the-app-to-the-topic).
90
+
91
+ ## 7. Read the logs
92
+
93
+ The app writes a log line for each thing it does, and Cloudflare keeps them for a few days, depending on your plan. Nothing else tells you if the once-a-minute schedule that sends posts stops, so it's worth knowing where to look.
94
+
95
+ 1. In the Cloudflare dashboard, go to **Workers & Pages**, select your Worker, the `name` in the `production` block of `wrangler.jsonc`, and select **Observability**.
96
+
97
+ 1. Filter on `event` equal to `sweep.tick`. A line arrives every minute.
98
+
99
+ 1. Filter on `event` equal to `send.completed`. Your send from section 4 is there.
100
+
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`.
102
+
103
+ 1. Filter on `level` equal to `error`. A healthy instance shows nothing.
104
+
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.
106
+
107
+ ## 8. Clean up the test subscribers
108
+
109
+ The checks leave test addresses on the list. Erase them, so the publisher starts with only real readers.
110
+
111
+ 1. In the editor, open **Subscribers**.
112
+
113
+ 1. Erase `bounce@simulator.amazonses.com` and `complaint@simulator.amazonses.com`. Open each one's menu (⋯), and select **Erase**.
114
+
115
+ 1. Erase your second address too, if it won't be a reader.
116
+
117
+ Erasing keeps only the address, so it's never mailed again, but the person can subscribe again through the subscribe page. Erasing your own address doesn't lock you out.
118
+
119
+ When every section gives the result it describes and AWS grants production access, your instance is ready to go live.
120
+
121
+ Next, [go live](08-go-live.md).
@@ -1,6 +1,6 @@
1
1
  # Go live
2
2
 
3
- At this point, your instance should be deployed, verified, and out of the SES sandbox, so it can send to anyone. From here, the publisher can set up the publication, and you can continue with the guides below.
3
+ At this point, your instance should be deployed and verified. Before anyone subscribes, check that AWS granted production access, as [Check that AWS granted it](06-production-access.md#4-check-that-aws-granted-it) describes, so SES can send to anyone. From here, the publisher can set up the publication, and you can continue with the guides below.
4
4
 
5
5
  ## Start publishing
6
6
 
@@ -8,7 +8,7 @@ The rest happens in the editor, at `https://newsletter.example.com/dashboard/`.
8
8
 
9
9
  If someone else is the publisher, let them sign in first:
10
10
 
11
- 1. Add their email address to the `Publishers` policy on the Access application, as in [Allow yourself in](03-access.md#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, store the list again with their address added.
11
+ 1. Add their email address to the policy on the Access application that lets you in, as in [Allow yourself in](03-access.md#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, store the list again with their address added.
12
12
 
13
13
  1. If they don't have a Cloudflare account, add another way to sign in, such as a one-time code by email. It's under **Zero Trust → Integrations → Identity providers**.
14
14
 
@@ -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
 
@@ -57,7 +57,7 @@ If you're the publisher, connect now, as [Connect Claude](../publishing/08-conne
57
57
 
58
58
  ## Who can connect
59
59
 
60
- Anyone the `Publishers` policy lets into the dashboard can connect Claude, since they sign in the same way. To stop someone connecting, remove them from that policy. If you set `ACCESS_ALLOWED_EMAILS`, store the list again without them: the app checks it on every request, so it takes effect at once.
60
+ Anyone the Access application's Allow policy lets into the dashboard can connect Claude, since they sign in the same way. To stop someone connecting, remove them from that policy. If you set `ACCESS_ALLOWED_EMAILS`, store the list again without them: the app checks it on every request, so it takes effect at once.
61
61
 
62
62
  ## Check it
63
63
 
@@ -25,7 +25,7 @@ A service token is a Client ID and a Client Secret that a program sends with eac
25
25
 
26
26
  ## 2. Allow the token in
27
27
 
28
- The application lets in only the people its `Publishers` policy names, so the token needs a policy of its own on the same application.
28
+ The application lets in only the people its Allow policy names, so the token needs a policy of its own on the same application.
29
29
 
30
30
  1. Under **Access controls → Applications**, select **Configure** on the application from [Lock the dashboard with Access](../get-started/03-access.md#2-create-the-access-application).
31
31
 
@@ -8,7 +8,7 @@ In this guide, you turn on Cloudflare's email for your app's hostname, and verif
8
8
 
9
9
  You need:
10
10
 
11
- - The notifications address saved, from [Verify it works](../get-started/05-verify.md#3-set-up-notifications).
11
+ - The notifications address saved, from [Verify it works](../get-started/07-verify.md#3-check-that-notifications-arrive).
12
12
  - Your domain's DNS on Cloudflare, as for the rest of the app.
13
13
 
14
14
  ## 1. Turn on Email Routing for your app's hostname
@@ -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/05-ses.md#8-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
 
@@ -82,7 +82,7 @@ If the test isn't delivered, **Last notification** shows Cloudflare's reason, su
82
82
 
83
83
  ## What notifications don't cover
84
84
 
85
- - **The schedule stopping.** The same once-a-minute schedule that sends posts also notices problems. If the schedule itself stops, nothing tells you. [Read the logs](../get-started/05-verify.md#7-read-the-logs) to check that `sweep.tick` still arrives every minute.
85
+ - **The schedule stopping.** The same once-a-minute schedule that sends posts also notices problems. If the schedule itself stops, nothing tells you. [Read the logs](../get-started/07-verify.md#7-read-the-logs) to check that `sweep.tick` still arrives every minute.
86
86
  - **A notification that can't get through.** The app tries each one up to five times, a minute apart. A failure shows under **Settings → Notifications**, and in the logs as `notify.failed`.
87
87
  - **Problems while no address is set.** With the address blank, the app sends nothing, and saves nothing up for later.
88
88
 
@@ -78,7 +78,7 @@ D1 keeps a history of your database, called Time Travel, for 30 days on Workers
78
78
  1. Note where the database stands now:
79
79
 
80
80
  ```bash
81
- npx wrangler d1 time-travel info kestrel-production
81
+ npx wrangler d1 time-travel info DB --env production
82
82
  ```
83
83
 
84
84
  It prints a bookmark for the database's current state. Copy it, and keep it until the upgrade checks out.
@@ -117,9 +117,9 @@ The output ends with your app's hostname.
117
117
 
118
118
  1. Reload the editor. The foot of the **Docs** tab's contents list shows the new version, which links to its release page.
119
119
 
120
- 1. Send yourself a test email, as in [Verify it works](../get-started/05-verify.md#1-send-yourself-a-test-email). Don't send a real post to check an upgrade: on a live instance, it goes to your whole list.
120
+ 1. Send yourself a test email, as in [Verify it works](../get-started/07-verify.md#1-send-yourself-a-test-email). Don't send a real post to check an upgrade: on a live instance, it goes to your whole list.
121
121
 
122
- 1. In the logs, `sweep.tick` still arrives every minute, as in [Verify it works](../get-started/05-verify.md#7-read-the-logs).
122
+ 1. In the logs, `sweep.tick` still arrives every minute, as in [Verify it works](../get-started/07-verify.md#7-read-the-logs).
123
123
 
124
124
  ## Go back
125
125
 
@@ -147,7 +147,7 @@ If the new release misbehaves, deploy the previous one, and put the database bac
147
147
  1. Restore the database to the bookmark from section 3:
148
148
 
149
149
  ```bash
150
- npx wrangler d1 time-travel restore kestrel-production --bookmark=REPLACE_WITH_BOOKMARK
150
+ npx wrangler d1 time-travel restore DB --env production --bookmark=REPLACE_WITH_BOOKMARK
151
151
  ```
152
152
 
153
153
  1. Push the revert to your repository:
@@ -1,6 +1,6 @@
1
1
  # As your list grows
2
2
 
3
- A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits. Workers Free suits a list of up to about 1,000 subscribers. Past about 5,000, one send can use up the day's database writes and stop the newsletter until midnight UTC. Two limits matter as the list grows: how fast SES lets you send, and how many database writes the free plan allows a day.
3
+ A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits. Workers Free suits a list of up to about 1,000 subscribers. Past about 5,000, one send can use up the day's database writes and stop the newsletter until midnight UTC. As the list grows, two limits start to matter: how fast a send goes out, and how many database writes the free plan allows a day.
4
4
 
5
5
  In this guide, you raise your SES limits, which helps on either plan, and upgrade to Workers Paid, which a larger list needs. Then you see how long a send takes after each.
6
6
 
@@ -10,26 +10,16 @@ SES caps how many emails your account sends a day, and how many a second. A send
10
10
 
11
11
  1. Ask AWS for a higher daily quota and a maximum send rate of 50, as AWS's [Increasing your Amazon SES sending quotas](https://docs.aws.amazon.com/ses/latest/dg/manage-sending-quotas-request-increase.html) describes.
12
12
 
13
- 1. When AWS grants it, the SES **Account dashboard** shows your new rate. In `wrangler.jsonc`, set the `production` block's `SES_MAX_SEND_RATE` to match, as [Configuration](../reference/01-configuration.md#vars) describes.
14
-
15
- 1. Commit the change, and push it:
16
-
17
- ```bash
18
- git commit -am "Match the SES send rate"
19
- git push
20
- ```
21
-
22
- 1. Deploy:
23
-
24
- ```bash
25
- npx wrangler deploy --env production
26
- ```
13
+ 1. When AWS grants it, the app takes the new rate from your account within about ten minutes, and **Settings → Email sender** shows it under **Send rate**. If `SES_MAX_SEND_RATE` is set in `wrangler.jsonc`, the app keeps to the lower of the two, so remove it or raise it to match, then deploy, as [Configuration](../reference/01-configuration.md#vars) describes.
27
14
 
28
15
  ## 2. Upgrade to Workers Paid
29
16
 
30
- On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) allows 100,000 written rows a day, reset at midnight UTC. A send writes about 14 rows a recipient, receipts included, and an import about 7 for each new subscriber. So at 1,000 subscribers, an import of the whole list and two sends on one day use about a third of the day. Once the day's rows are spent, the whole app stops until midnight UTC, the SES webhook with it, so that day's bounces and complaints are lost, and suppress or unsubscribe no one.
17
+ Cloudflare's $5 Workers Paid plan helps in two ways, at any size of list.
18
+
19
+ - **Faster sends.** On the free plan, Cloudflare caps how much one minute's work can do. A send then uses your SES rate for only a few seconds of each minute: about 56 emails a minute at a rate of 14. On Workers Paid, a send uses about 50 seconds of each minute, about 700 emails a minute at the same rate. If sends feel slow, upgrading speeds them up more than raising your SES rate does. [How long a send takes](#how-long-a-send-takes) compares them.
20
+ - **No daily limit on database writes.** On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) allows 100,000 written rows a day, reset at midnight UTC. A send writes about 14 rows a recipient, receipts included, and an import about 7 for each new subscriber. So at 1,000 subscribers, an import of the whole list and two sends on one day use about a third of the day. Once the day's rows are spent, the whole app stops until midnight UTC, the SES webhook with it. That day's bounces and complaints are lost, and suppress or unsubscribe no one.
31
21
 
32
- It's recommended to upgrade from about 1,000 subscribers. Past that, the Dashboard and the Subscribers screen say so, and so does an import that would take the list past it. Past about 5,000, a send that could use up the day on its own also warns before it's scheduled. The app doesn't count the day's writes, so between the two, two large sends or several imports and a send on one UTC day can still use it up without a warning at that moment.
22
+ It's recommended to upgrade from about 1,000 subscribers. Past that, the Dashboard and the Subscribers screen say so, and so does an import that would take the list past it. Past about 5,000, a send that could use up the day on its own also warns before it's scheduled. The app doesn't count the day's writes, so between the two, two large sends or several imports and a send on one UTC day can still use it up without a warning at that moment.
33
23
 
34
24
  1. Upgrade your Cloudflare account to the $5 [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/), which has no daily limit on writes.
35
25
 
@@ -61,6 +51,6 @@ Over years, one more limit can matter: on the free plan, one [D1 database](https
61
51
 
62
52
  ## Check it
63
53
 
64
- 1. The SES **Account dashboard** shows the maximum send rate you set in `SES_MAX_SEND_RATE`.
54
+ 1. **Settings → Email sender** shows your new maximum send rate under **Send rate**, "from your SES account".
65
55
 
66
56
  1. On Workers Paid, the import screen under **Subscribers → Import** reads "CSV or .txt, up to 10,000 rows."
@@ -9,14 +9,14 @@ This page covers what Kestrel handles, what you handle, and how to act on a read
9
9
  - **Consent.** Kestrel mails only people who confirmed they want your newsletter. Someone who subscribes gets an email with a link, and only once they confirm do they receive posts. People you import come with your old service's record of their consent, and your word for it ([Bring your existing list](01-your-list.md#what-carries-over)).
10
10
  - **Unsubscribing.** Every email has an unsubscribe link, and the one-click unsubscribe header that Gmail and Yahoo ask bulk senders for. It takes effect at once. US law allows up to 10 business days.
11
11
  - **Bounces and complaints.** An address that bounces is never mailed again, so a bad address can't keep hurting your sending reputation. A reader who marks your email as spam is unsubscribed, as if they had used the link, and you can't add them back: only their own sign-up does.
12
- - **Sender authentication.** Your emails are signed with DKIM for your own domain, and SPF and DMARC records in its DNS vouch for them. You set them up in [Connect Amazon SES](../get-started/04-ses.md), and mailbox providers now expect them.
12
+ - **Sender authentication.** Your emails are signed with DKIM for your own domain, and SPF and DMARC records in its DNS vouch for them. You set them up in [Connect Amazon SES](../get-started/05-ses.md), and mailbox providers now expect them.
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/05-ses.md#8-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
 
@@ -4,6 +4,8 @@ Give your publication a name and a look before your first post. Set the name, ta
4
4
 
5
5
  All three are optional, and you can change them at any time. How they appear in each email comes from your email template, which you can [edit](11-email-template.md).
6
6
 
7
+ Write the tagline as one line that says what the newsletter covers and how often it goes out, such as "Monthly notes on the birds of the Hudson Valley." Until your first post, it's the only line on your home page that says what readers are signing up for. It's also what AWS sees if it reviews your site when you [request production access](../get-started/06-production-access.md).
8
+
7
9
  ## When to provide a mailing address
8
10
 
9
11
  **If your newsletter is mostly your own writing, you probably don't need one. If an email is mainly selling something, add one.**
@@ -16,5 +18,5 @@ This is general information, not legal advice. The FTC's [CAN-SPAM compliance gu
16
18
 
17
19
  ## Who sets the From name and address
18
20
 
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).
21
+ 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
22
 
@@ -16,7 +16,7 @@ The editor takes Markdown, and Kestrel saves as you type. The same Markdown beco
16
16
 
17
17
  Choose **Schedule**, pick a time under **Send at**, and confirm. The time must be at least five minutes away by default, or the lead whoever runs your instance set. That's your window to catch a mistake, and you can cancel or move the send until then. **Send now** sends after the same wait.
18
18
 
19
- **Then send yourself a test.** Scheduling locks the post and makes its email, so **Send test email** now sends exactly what your list receives. **Settings → Default test recipients** fills in the addresses. A test needs an email provider, which [Connect Amazon SES](../get-started/04-ses.md) sets up.
19
+ **Then send yourself a test.** Scheduling locks the post and makes its email, so **Send test email** now sends exactly what your list receives. **Settings → Default test recipients** fills in the addresses. A test needs an email provider, which [Connect Amazon SES](../get-started/05-ses.md) sets up.
20
20
 
21
21
  To change the post, choose **Cancel** on its banner, and it's a draft again. **Edit schedule** moves the time without unlocking it.
22
22
 
@@ -1,108 +1,73 @@
1
1
  # Troubleshooting
2
2
 
3
- When a send needs you, its page and the dashboard's health block say so in a red or amber line. Red needs you to act, and amber is worth a look. Find the line's words below for what it means and what to do.
3
+ When something needs you, Kestrel says so in a line on the send's page, under **Sent**. The top of the dashboard shows it too. Red needs you to act, and amber is worth a look. The line says what happened, what to do, and who does it. This page covers what the screen can't: the fixes made outside Kestrel, and the decisions that are yours.
4
4
 
5
5
  **Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**. With none set, Kestrel emails you nothing. [Notifications](06-troubleshooting.md#notifications), below, lists what you're told.
6
6
 
7
- ## A send is waiting or held
7
+ ## A send is held or waiting
8
8
 
9
- A problem with your email provider never marks anyone unsent. The send waits where it is, and carries on from there once the problem clears. "No one has been marked unsent because of this" means this problem: readers whose own email failed earlier stay unsent.
9
+ Nothing is lost. A problem with your email provider never marks anyone unsent or mails anyone twice. The send waits where it is, and carries on once the problem is fixed. The send's line names the fix. When that fix is outside Kestrel, whoever runs your instance makes it, and it's one of these:
10
10
 
11
- - **"The provider is refusing the account".** Your provider won't take mail from your account, and its own words follow, with the fix and who makes it. The fix is almost always in your Amazon SES account or your instance, made by whoever runs it. [What SES refuses, and the fix](#what-ses-refuses-and-the-fix), below, lists each one. Any other post that comes due meanwhile waits with it, without asking SES, and you're told once. While a quota frees up a few at a time, or while the SES sandbox takes only your verified addresses, SES takes some readers on each retry and refuses the rest: those it takes are sent, and the line stays, with the time it began, until SES takes everyone it's asked to.
12
- - **"The provider is refusing this send".** SES refuses this post's email itself, or a batch of it as invalid, rather than your account. Its words follow. Other posts go out as usual. If SES's words name a reader's address, remove that subscriber: the send skips them and carries on at its next retry. If SES refuses the email itself, the email can't change once its send has started, so whoever runs your instance asks AWS Support why, quoting SES's words.
13
- - **"No email provider is connected".** The send is held until one is connected ([Connect Amazon SES](../get-started/04-ses.md)), and goes out once it is.
14
- - **"The provider is unavailable".** An outage or a rate limit on the provider's side. The send reads **Backing off** and says when it next retries, spacing its retries out up to an hour apart while the outage lasts, so there's nothing to do. Any other post that comes due meanwhile waits with it.
15
- - **"The provider keeps refusing for its sending rate".** An amber line. SES has refused the first request of several retries in a row for its sending rate, which a short burst never does, so Kestrel is set to send faster than your SES account allows. Whoever runs your instance sets `SES_MAX_SEND_RATE` to the **Maximum send rate** on the SES console's **Account dashboard** (1 a second in the sandbox), and deploys, as [Connect Amazon SES](../get-started/04-ses.md#8-set-the-provider-and-deploy) describes. The send retries on its own meanwhile.
16
- - **"Nothing has gone out for …".** A send has handed off no one for half an hour, with no retry or provider problem to explain it. The part of Kestrel that sends has stopped or keeps failing on this send, or another send going out at the same time is taking all of its turn. A large send going out slowly never shows this. If another send is going out, this one picks up once it finishes. Read the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes, and check the Worker's trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes. The send carries on from where it stopped once it runs again, mailing no one twice.
17
- - **"The fire time passed … and the send has not started".** The Cloudflare Cron Trigger that starts sends on time isn't running. The send goes out as soon as it runs again. Check the Worker's trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes.
18
- - **"The provider never answered for …".** Rarely, Kestrel can't tell whether your provider took some emails, and sending again could mail those readers twice. **Resolve…** on the send's page asks you to decide. **Assume not sent** is the safe default: those readers may miss this post, but get your next one. Choose **Assume sent** only when you know they got it.
11
+ - **Amazon SES refuses your account or the send.** The line carries SES's words and the fix, almost always in the SES console. [Connect Amazon SES](../get-started/05-ses.md) sets up each piece SES checks: the sending domain, the AWS key, and the configuration set. [Request production access](../get-started/06-production-access.md) lifts the sandbox. When SES refuses your account, any other post that comes due meanwhile waits with it. When it refuses only this send's email, other posts go out as usual. While SES takes some readers and refuses the rest, as while a daily quota frees up, those it takes are sent. The line stays until SES takes everyone.
12
+ - **SES keeps refusing for its sending rate.** Kestrel is sending faster than your SES account allows. **Settings → Email sender** shows the **Send rate** and where it comes from. If `SES_MAX_SEND_RATE` sets it, lower it to the maximum send rate on the SES console's account dashboard, or remove it, then deploy, as [Configuration](../reference/01-configuration.md#vars) describes. If it's Kestrel's default, the app can't read your account: allow its key `ses:GetAccount`, as [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) shows.
13
+ - **No email provider is connected.** Connect one, as [Connect Amazon SES](../get-started/05-ses.md) describes, and the send goes out at its next retry.
14
+ - **A send missed its time, or nothing has gone out for a while.** The part of Kestrel that sends has stopped, or keeps failing on this send. Check the Worker's Cron Trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes, and read the app's logs, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes. If another send is going out at the same time, this one may only be waiting its turn.
19
15
 
20
- ## Before a send
16
+ ## Someone was mailed twice, or not at all
21
17
 
22
- - **A test says "The provider refused it".** Your provider didn't take the test, and its own words follow, with the fix where Kestrel knows it. Sending again gets the same answer until that's fixed. While Amazon SES is in its sandbox, it sends only to addresses verified in SES, so verify the address you test with in the SES console, or request production access, as [Connect Amazon SES](../get-started/04-ses.md#5-request-production-access) describes. Words about the account, such as a key or the sending domain, point at the same fix as [a send the provider refuses](06-troubleshooting.md#a-send-is-waiting-or-held).
23
- - **A test says "The provider didn't answer, so the test may have arrived".** Kestrel can't tell whether your provider took it. Check the inbox before you send again, since a second test may arrive twice.
24
- - **"The template or publication details changed this email's look after your last test".** Send a new test of the post, since your last one no longer shows what goes out.
25
- - **The daily database writes warning.** On Cloudflare's free Workers plan, a send to more than about 5,000 recipients may use up the day's writes, and then the app stops working until midnight UTC. [Bring your existing list](01-your-list.md#limits) explains the limit, and when to upgrade to Workers Paid.
26
- - **"Workers Free suits a list of up to about 1,000 subscribers".** Your list is past what the free plan suits. Upgrade to Workers Paid ([As your list grows](../guides/09-as-your-list-grows.md)). If the account is on Workers Paid already, set `SUBREQUEST_BUDGET` so Kestrel knows, and the notice goes.
18
+ Kestrel never mails a reader twice on its own. When it can't tell whether your provider took some emails, it stops and asks you: the send's page shows **Resolve…**. Choose **Assume not sent** unless you know otherwise, since it's the safe default: those readers may miss this post, but get your next one. Choose **Assume sent** only when you know they got it, such as from a reader who tells you so. Either choice mails no one.
27
19
 
28
- ## Reading a send's record
20
+ A reader recorded as **Unsent** is still on your list and gets your next post. That send never mails them again: their own email kept failing, SES won't take their address, or you chose **Assume not sent**.
29
21
 
30
- A send's page becomes its permanent record once it finishes. Its counts cover everyone the send went to:
31
-
32
- | Count | What it means |
33
- | --- | --- |
34
- | **Delivered** | Your provider confirmed the email reached the reader's mail server. |
35
- | **Bounced** | The receiving server turned the email away. |
36
- | **Complained** | The reader marked the email as spam. |
37
- | **Unsent** | Kestrel couldn't hand the email to your provider. |
38
-
39
- Delivered always means confirmed, not just accepted by your provider. Confirmations can lag by days, so counts keep rising after a send finishes. Some emails are never confirmed at all, so a send can read **Settling** for good, which is normal, as long as some receipts have come.
22
+ ## A reader says they never got a post
40
23
 
41
- **"No delivery receipt has arrived for any of …".** An hour after a send went out, Kestrel has heard back about none of its readers. Your provider's reports probably aren't reaching Kestrel, so its bounces suppress no one and its complaints unsubscribe no one, and your next post mails those addresses again. Look for `webhook.refused` errors in the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes. It names the SNS topic that reached Kestrel and the one it expects. Then check the subscription in [Connect Amazon SES](../get-started/04-ses.md#9-subscribe-the-app-to-the-topic). The line goes as soon as a receipt arrives.
24
+ Under **Subscribers**, search for their address and open their row. Their page lists every send they were in, newest first, with what happened on each: **Delivered**, a bounce, a complaint, **Unsent** with your email provider's words, or **Skipped** because they had unsubscribed or were suppressed by then. Each send links to its record. The page also shows how they joined and why they left, if they did. Any bounce or block on the address shows with the provider's words or yours. If they aren't on the list, or are still **pending**, they never confirmed, and only confirmed subscribers are sent your posts.
42
25
 
43
- **Unsent recipients** are still on your list, and get your next post. A recipient is unsent when their own email kept failing, or their address isn't one SES accepts, with the provider's words on the row, or when you chose **Assume not sent**.
26
+ ## A send still reads Settling
44
27
 
45
- ### What SES refuses, and the fix
28
+ That's normal. **Delivered** counts only the emails your provider confirmed reached the reader's mail server. Confirmations can lag by days, so the counts keep rising after a send finishes. Some are never confirmed at all, so a send can read **Settling** for good, as long as some receipts have come.
46
29
 
47
- When SES refuses your account or a send, the red line carries SES's words and the fix. Almost every fix is made by whoever runs your instance, in the SES console or its settings.
30
+ When none come, the send's page says so an hour after it went out. Your provider's reports aren't reaching Kestrel, so its bounces suppress no one and its complaints unsubscribe no one. Your next post mails those addresses again. Look for `webhook.refused` errors in the app's logs, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes. Each names the SNS topic that reached Kestrel and the one it expects. Then check the subscription, as [Connect Amazon SES](../get-started/05-ses.md#7-subscribe-the-app-to-the-topic) describes.
48
31
 
49
- | SES says | What it means | The fix |
50
- | --- | --- | --- |
51
- | Email address is not verified, naming a reader | The SES account is still in the sandbox, which mails only verified addresses. | Request production access on the SES console's **Account dashboard**, as [Connect Amazon SES](../get-started/04-ses.md#5-request-production-access) describes. Signups fail until then, and you're emailed about it once a day. |
52
- | Email address is not verified, naming your From address or its domain | The sending identity isn't verified in this SES region. | Verify the From address or its domain in SES, in the region your instance sends from. |
53
- | Mail FROM domain is not verified | SES can't read the custom MAIL FROM domain's MX record. | Check that record in DNS, or set the identity's **Behavior on MX failure** to use the default MAIL FROM domain. |
54
- | Daily message quota exceeded | The 24-hour sending quota is spent. It frees up on its own. | In the sandbox (200 a day), request production access. Otherwise, ask AWS for a higher quota, as [As your list grows](../guides/09-as-your-list-grows.md) describes. |
55
- | Sending paused (account) | AWS paused the account under review, or sending was turned off by hand. | Follow AWS's email to the AWS account's address, or turn sending back on. |
56
- | Sending paused (configuration set) | Sending is turned off on the configuration set, by hand or by an alarm. | Turn sending back on for that set. |
57
- | Account suspended | AWS shut down the account's sending. | Only AWS can lift it: open a case with AWS Support. |
58
- | An access key, signature, or permission error | Your instance's SES key is wrong, expired, or lacks permission. | Replace the key, or give it `ses:SendEmail` and `ses:SendBulkEmail`. |
59
- | A subscription is needed (OptInRequired) | The AWS account isn't signed up for SES. | Enable SES for the account, or settle its activation or billing with AWS. |
60
- | Configuration set does not exist | `SES_CONFIGURATION_SET` names a set that isn't in this region. | Fix the name, or create the set. |
61
- | Sending pool not found | The configuration set uses a dedicated IP pool that doesn't exist. | Pick an existing pool in the set's sending options, or remove it. |
62
- | The message was rejected | SES refuses this email itself. | Ask AWS Support why. Other posts go out as usual. |
63
- | The request is invalid | SES refused a batch of this send. | If SES's words name a reader's address, remove that subscriber. |
32
+ ## A test doesn't arrive
64
33
 
65
- A server error from SES may come after SES already took the emails, so Kestrel never sends that batch again: its readers wait for **Resolve…**, as for "The provider never answered for …".
34
+ While Amazon SES is in its sandbox, it mails only addresses verified in SES. Verify the address you test with in the SES console, or [request production access](../get-started/06-production-access.md). When the test's line says the provider didn't answer, check the inbox before you send it again, since the first may have arrived. A test SES refuses for your account has the same fix as [a send it refuses](06-troubleshooting.md#a-send-is-held-or-waiting).
66
35
 
67
- ## A reader says they never got a post
36
+ ## Your list outgrows Workers Free
68
37
 
69
- Under **Subscribers**, search for their address and open their row. Their page lists every send they were in, newest first, with what happened on each: **Delivered**, a bounce, a complaint, **Unsent** with your email provider's words, or **Skipped** because they had unsubscribed or were suppressed by then. Each send links to its record. The page also shows how they joined, any bounce or block on the address with the provider's words or yours, and why they left, if they did. If they aren't on the list, or are still **pending**, they never confirmed, and only confirmed subscribers are sent your posts.
38
+ Upgrade the Cloudflare account to Workers Paid, and set `SUBREQUEST_BUDGET` so Kestrel sends faster, as [As your list grows](../guides/09-as-your-list-grows.md) describes. Kestrel warns you before a send that could use up the free plan's daily database writes, which stops the app until midnight UTC. [Bring your existing list](01-your-list.md#limits) explains the limits.
70
39
 
71
40
  ## Bounces and complaints
72
41
 
73
42
  Kestrel acts on these for you, so a bad address can't keep hurting your sending reputation:
74
43
 
75
- - **A hard bounce** means the address can't receive mail. Kestrel never mails it again.
76
- - **A soft bounce** is temporary, such as a full mailbox. The address stays on your list.
77
- - **A complaint** means the reader marked your email as spam. Kestrel unsubscribes them, whichever email they reported, and their page, opened from their row, says they left by a complaint.
44
+ - A hard bounce means the address can't receive mail. Kestrel never mails it again.
45
+ - A soft bounce is temporary, such as a full mailbox. The address stays on your list.
46
+ - A complaint means the reader marked your email as spam. Kestrel unsubscribes them, whichever email they reported, and their page, opened from their row, says they left by a complaint.
78
47
 
79
- Under **Subscribers**, a bounced address keeps its status and carries a label saying why, and its page, opened from its row, gives your email provider's words for the bounce and when it happened. When a recent send's bounces reach about 5% of its recipients, the dashboard warns you. That's about the rate at which email providers start to review a sender, so check the addresses on its record.
48
+ Under **Subscribers**, a bounced address keeps its status and carries a label saying why. Its page gives your email provider's words for the bounce and when it happened. When a recent send's bounces reach about 5% of its recipients, the dashboard warns you. That's about the rate at which email providers start to review a sender, so check the addresses on its record.
80
49
 
81
50
  ### Lifting a suppression
82
51
 
83
- Lift one only when you know the address works again, such as a reader who tells you their mailbox is fixed. Mailing an address that keeps failing can hurt every send after it. In a subscriber's ⋯ menu, **Unblock** lifts a block and **Clear** lifts a bounce, after asking you to confirm. A complaint isn't a suppression but the reader leaving, so nothing lifts it: they come back only by subscribing again themselves, and **Add subscriber** refuses them. Past records keep the bounce or complaint where it happened.
52
+ Lift one only when you know the address works again, such as a reader who tells you their mailbox is fixed. Mailing an address that keeps failing can hurt every send after it. In a subscriber's ⋯ menu, **Unblock** lifts a block and **Clear** lifts a bounce, after asking you to confirm. A complaint isn't a suppression but the reader leaving, so nothing lifts it. They come back only by subscribing again themselves, and **Add subscriber** refuses them. Past records keep the bounce or complaint where it happened.
84
53
 
85
54
  ## An import is refused
86
55
 
87
- Kestrel refuses a file it can't read with certainty, and says what's wrong and where. Nothing on your list changes.
88
-
89
- - **Not in the format.** The file isn't a Kestrel CSV or a plain list. Convert it first, with Claude or by hand.
90
- - **A column or value the format doesn't take,** named with its row and column. Fix it in the file, or move a column of your old service's into `evidence`.
91
- - **Too many rows** for one import. Split the file.
92
- - **Too large.** An import takes up to 10 MB.
93
-
94
- [The file format](01-your-list.md#the-file-format) describes what each column takes.
56
+ Nothing on your list changes. Kestrel refuses a file it can't read with certainty, and names the row and column it couldn't. Fix the file and import it again. Convert a file in another format first, with Claude or by hand. Move a column of your old service's into `evidence`. Split a file of more than 10 MB, or with too many rows. [The file format](01-your-list.md#the-file-format) describes what each column takes.
95
57
 
96
58
  ## Claude can't connect
97
59
 
98
- [Connect Claude](08-connect-claude.md) covers connecting it. When that fails, find what Claude or the dashboard says:
60
+ Most failures are in your instance's Cloudflare Access setup, made by whoever runs your instance. [Connect Claude](08-connect-claude.md) covers connecting it. When that fails:
99
61
 
100
62
  - **Claude says it couldn't connect, or the sign-in failed.** Your instance's Access application needs Managed OAuth set up as [Turn on Managed OAuth](../guides/01-allow-claude.md#1-turn-on-managed-oauth) describes, including Claude's sign-in address for Claude Desktop and claude.ai. Check that the connector's address is your publication's address followed by `/api/mcp`, and that your instance is deployed, not running on your own computer.
101
63
  - `kestrel login` says your instance "doesn't offer a sign-in for programs". Managed OAuth is off. Turn it on, as the same section describes.
102
- - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the address you signed in with. If `kestrel login` names `access_unidentified` after the `401`, or `kestrel api` prints `reason: access_unidentified`, it's the next problem: both then say that Access sent neither an email address nor a service token. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
103
- - **"Access let you in, but didn't say who you are".** The editor shows this, and the API refuses with a `401` whose `reason` is `access_unidentified`, when your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the `Publishers` policy. Whoever runs your instance removes the other policy from its Access application, or changes it to one of those two kinds.
104
- - **Claude's changes read "via the API".** A request made with your sign-in named no client, such as one from Claude calling the API some way other than `kestrel api`, which names Claude Code. Your instance's instructions for Claude Code have it use `kestrel api`, once they're brought up to date, as [Install the release](../guides/07-upgrade.md#2-install-the-release) describes. Changes reading "a service token" mean Claude still uses one, and [Connect Claude](08-connect-claude.md#claude-code) signs it in as you instead.
105
- - A script gets a `401` whose `reason` is `access_unidentified`. Access let the script in through a policy Kestrel doesn't accept. It needs a service token under a Service Auth policy, as [Allow the token in](../guides/02-automate.md#2-allow-the-token-in) describes.
64
+ - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the address you signed in with. If either names `access_unidentified`, it's the next problem. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
65
+ - **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the policy that lets you in by email. Whoever runs your instance removes the other policy from its Access application, or changes it to one of those two kinds. A script that gets this needs a service token under a Service Auth policy, as [Allow the token in](../guides/02-automate.md#2-allow-the-token-in) describes.
66
+ - **Claude's changes read "via the API".** Claude called the API some way other than `kestrel api`, which names Claude Code. Your instance's instructions for Claude Code have it use `kestrel api` once they're brought up to date, as [Install the release](../guides/07-upgrade.md#2-install-the-release) describes. Changes reading "a service token" mean Claude still uses one, and [Connect Claude](08-connect-claude.md#claude-code) signs it in as you instead.
67
+
68
+ ## Cloudflare won't save the Access application
69
+
70
+ Whoever sets up your instance can meet this in [Lock the dashboard with Access](../get-started/03-access.md#2-create-the-access-application). Saving the application fails with an error naming `use_clientless_isolation_app_launcher_url`, saying it needs private destinations. The form has an empty private hostname or IP row beside the two public hostnames. Remove that row, and save again.
106
71
 
107
72
  ## Notifications
108
73
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Your publication's subscribe page is at `/subscribe` on your publication's address, such as `https://newsletter.example.com/subscribe`. Copy it from the dashboard's checklist or **Settings → Ways to subscribe**, and share it wherever your readers are. A reader joins only once they confirm by email.
4
4
 
5
- **Signups open only once an email provider is connected.** Until then, the page says the newsletter isn't taking subscribers yet, and the dashboard says so too. You can share the link anyway, and it opens once email is connected ([Connect Amazon SES](../get-started/04-ses.md)).
5
+ **Signups open only once an email provider is connected.** Until then, the page says the newsletter isn't taking subscribers yet, and the dashboard says so too. You can share the link anyway, and it opens once email is connected ([Connect Amazon SES](../get-started/05-ses.md)).
6
6
 
7
7
  ## How a reader joins
8
8
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  Before your readers get anything, check three things yourself: your email arrives and replies reach you, subscribing works, and problems will reach you.
4
4
 
5
- **Email has to be connected first.** Until it is, nothing can be sent, and the dashboard's checklist shows **Connect Amazon SES** in place of this step, as [Connect Amazon SES](../get-started/04-ses.md) describes.
5
+ **Email has to be connected first.** Until it is, nothing can be sent, and the dashboard's checklist shows **Connect Amazon SES** in place of this step, as [Connect Amazon SES](../get-started/05-ses.md) describes.
6
+
7
+ **Until AWS grants production access, Amazon SES mails only addresses verified in SES.** So test with a verified address, and hold the subscribe link until then. The checklist adds **Request production access** after this step, and checks it off once AWS grants it, as [Request production access](../get-started/06-production-access.md) describes.
6
8
 
7
9
  ## 1. Send yourself a test
8
10
 
@@ -10,7 +12,7 @@ Choose **Template** in the sidebar, then **Send test email**, and send a sample
10
12
 
11
13
  - **The From name and address,** which readers see first. They're set when your instance is deployed.
12
14
  - **Where it lands:** the inbox, Promotions, or spam. If it lands in spam, check your sending domain's records, as [Sending-domain DNS](../reference/02-sending-domain-dns.md) describes.
13
- - **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/04-ses.md#3-receive-replies-at-your-from-address).
15
+ - **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/05-ses.md#8-receive-replies-at-your-from-address).
14
16
 
15
17
  ## 2. Subscribe yourself
16
18