@kurtbruns/kestrel 1.19.0 → 1.21.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 (33) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/bin/kestrel.mjs +1 -1
  3. package/cli/seed.mjs +2 -8
  4. package/dist/public/dashboard/app.js +307 -306
  5. package/dist/public/dashboard/app.js.map +4 -4
  6. package/dist/public/dashboard/index.html +2 -2
  7. package/dist/public/dashboard/styles.css +90 -51
  8. package/dist/worker/index.js +890 -559
  9. package/docs/get-started/02-deploy.md +3 -3
  10. package/docs/get-started/03-access.md +8 -4
  11. package/docs/get-started/04-aws-account.md +2 -0
  12. package/docs/get-started/05-ses.md +4 -2
  13. package/docs/get-started/07-verify.md +8 -6
  14. package/docs/get-started/08-go-live.md +12 -18
  15. package/docs/guides/01-allow-claude.md +7 -2
  16. package/docs/guides/03-notifications.md +17 -13
  17. package/docs/guides/07-upgrade.md +16 -2
  18. package/docs/publishing/03-name-your-publication.md +8 -0
  19. package/docs/publishing/04-write-a-post.md +2 -2
  20. package/docs/publishing/06-troubleshooting.md +52 -26
  21. package/docs/publishing/11-email-template.md +1 -1
  22. package/docs/publishing/README.md +1 -1
  23. package/docs/reference/02-sending-domain-dns.md +3 -3
  24. package/migrations/0021_notifications_outage_receipts.sql +46 -0
  25. package/package.json +10 -10
  26. package/packages/kestrel/lib/api.mjs +19 -12
  27. package/packages/kestrel/lib/login.mjs +2 -2
  28. package/packages/kestrel/lib/media-types.mjs +34 -0
  29. package/packages/kestrel/lib/oauth.mjs +19 -2
  30. package/packages/kestrel/lib/openapi.mjs +1 -1
  31. package/template/README.md +1 -1
  32. package/template/claude/CLAUDE.md +1 -1
  33. package/template/claude/skills/upgrade/SKILL.md +1 -0
@@ -167,7 +167,7 @@ Create Kestrel's tables in the production database:
167
167
  npx wrangler d1 migrations apply DB --remote --env production
168
168
  ```
169
169
 
170
- It lists the migrations it's about to apply and asks you to confirm. It applies only the ones the database hasn't seen, so running it again is harmless.
170
+ It lists the migrations it's about to apply and asks you to confirm. Run by Claude, or in any other shell that isn't interactive, it answers its own prompt and applies them without asking. It applies only the ones the database hasn't seen, so running it again is harmless.
171
171
 
172
172
  ## 6. Deploy
173
173
 
@@ -175,7 +175,7 @@ It lists the migrations it's about to apply and asks you to confirm. It applies
175
175
  npx wrangler deploy --env production
176
176
  ```
177
177
 
178
- 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.
178
+ This deploys the release your instance runs, with your settings. Near its end, the output names the hostname the app now answers on, then its schedule and a version ID. 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.
179
179
 
180
180
  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 that answers nowhere and never sends. It takes the top level's `name` in `wrangler.jsonc`, such as `newsletter-dev`. If that happens, remove it by that name:
181
181
 
@@ -195,7 +195,7 @@ npx wrangler delete --name newsletter-dev
195
195
  {"status":"ok","service":"kestrel"}
196
196
  ```
197
197
 
198
- If `curl` reports a certificate error, or says it could not resolve the host, Cloudflare is still setting up the new hostname. Try again in a few minutes.
198
+ If `curl` reports a certificate error or can't resolve the host, Cloudflare is still setting up the hostname. Or your computer remembers it from before it existed. Try again over the next 30 minutes.
199
199
 
200
200
  1. The editor is locked. Until you set up Access in the next step, the app can't tell who is asking, so it lets no one in:
201
201
 
@@ -37,7 +37,7 @@ The editor lives under `/dashboard`, and the API under `/api`. One application c
37
37
 
38
38
  Each path covers everything under it. Leave every other path out. The home page, the archive, subscribing, unsubscribing, images, and your email provider's webhooks must stay public.
39
39
 
40
- 1. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it, or saving fails with an error naming `use_clientless_isolation_app_launcher_url`.
40
+ 1. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it. Otherwise saving fails with an error naming `use_clientless_isolation_app_launcher_url`, saying it needs private destinations.
41
41
 
42
42
  1. Name the application after your app's hostname, `newsletter.example.com`. Cloudflare suggests a name made from the subdomain, which won't tell two newsletters apart.
43
43
 
@@ -55,7 +55,7 @@ Every Access application denies everyone until a policy allows them.
55
55
 
56
56
  1. Select **Create**.
57
57
 
58
- Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made 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**.
58
+ Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made should be one.
59
59
 
60
60
  ## 4. Give the app its two values
61
61
 
@@ -93,8 +93,6 @@ The app verifies each login against your team domain, and checks that it was iss
93
93
 
94
94
  Spaces and capital letters don't matter. Without the list, the app lets in anyone your Access policy allows.
95
95
 
96
- To let someone else into the editor, such as the publisher or a co-editor, add their email address to the policy from [Allow yourself in](#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, run the command again and enter the whole list with their address added, since the new value replaces the old one.
97
-
98
96
  ## Check it
99
97
 
100
98
  1. In a private browser window, open `https://newsletter.example.com/dashboard/`. Cloudflare asks you to sign in.
@@ -123,4 +121,10 @@ If a check fails:
123
121
  - **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. The same goes for anyone else you let in.
124
122
  - **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 policy you made above.
125
123
 
124
+ ## Allow someone else in
125
+
126
+ To let someone else into the editor, such as the publisher or a co-editor, add their email address to the policy from [Allow yourself in](#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, run the command again and enter the whole list with their address added, since the new value replaces the old one.
127
+
128
+ If they don't have a Cloudflare account, add another way to sign in, such as a one-time code by email, under **Zero Trust → Integrations → Identity providers**.
129
+
126
130
  Next, [create an AWS account](04-aws-account.md).
@@ -65,4 +65,6 @@ AWS charges for what you use, with no spending limit, so a mistake or a leaked k
65
65
 
66
66
  1. Under **Budgets**, your budget is listed, and its email recipient shows as **Active**.
67
67
 
68
+ If it shows as **Pending**, select **Resend verification** on its row, and verify the address as section 4 describes.
69
+
68
70
  Next, [connect Amazon SES](05-ses.md).
@@ -47,9 +47,11 @@ The same form sets a custom MAIL FROM domain, so SPF also passes for your domain
47
47
 
48
48
  1. Wait until SES shows **Identity status** as **Verified**, and both **DKIM configuration** and **MAIL FROM configuration** as **Successful**.
49
49
 
50
+ If they stay pending, check that each record's proxy status is **DNS only**.
51
+
50
52
  ## 2. Add a DMARC record
51
53
 
52
- 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.
54
+ DMARC tells inboxes what to do with mail that fails its checks, and can ask them to send you reports. The `rua` tag that asks for them is optional, and the policy works without it. SES doesn't add this record, so you add it yourself. It starts in monitor mode, `p=none`, which blocks nothing, your own mail included. It only monitors: inboxes still deliver mail that fails, so it doesn't stop anyone sending as your domain.
53
55
 
54
56
  1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
55
57
 
@@ -307,7 +309,7 @@ Replies to a post go to your From address, `newsletter@send.example.com`, which
307
309
 
308
310
  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.
309
311
 
310
- 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`.
312
+ 1. **(Optional)** Forward your DMARC reports too, if you collect them and 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`. Reports come about once a day from each inbox provider that received your mail. A day or two after your next email goes out, check that they arrive. Email Routing's **Activity log** shows each forward's status.
311
313
 
312
314
  1. Check that your From address receives mail. This now lists Cloudflare's three `route` mail servers:
313
315
 
@@ -17,7 +17,7 @@ A test email goes through the same code as a real send. If the test looks right,
17
17
 
18
18
  1. In the editor at `https://newsletter.example.com/dashboard/`, create a post with a subject, a short body, and an image.
19
19
 
20
- 1. Select **Send test email**, enter your address, and send it.
20
+ 1. Select **Send test email**, enter your address, and send it. If SES refuses it, the test's line says why and what fixes it.
21
21
 
22
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
23
 
@@ -45,7 +45,7 @@ Kestrel emails each notifications address when a send finishes, and right away w
45
45
 
46
46
  1. In the editor, open **Settings → Notifications**, type that address under **Send notifications to**, select **Add inbox**, and then **Save**.
47
47
 
48
- 1. Select **Send a test notification**. It arrives at that address.
48
+ 1. Select **Send a test notification**. It arrives at that address. If it doesn't, **Last notification**, on the same page, says what happened.
49
49
 
50
50
  The publisher sets their own address in [Check your setup](../publishing/10-check-your-setup.md).
51
51
 
@@ -65,11 +65,13 @@ This is a real send, to a list of one. It proves the whole path: subscribing, co
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 the send shows in red under **Sent**, its line names the fix. Once that's fixed, the send resumes on its own.
69
+
68
70
  ## 5. Unsubscribe
69
71
 
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.
72
+ Every email carries an unsubscribe link, and a header that lets mail clients show their own **Unsubscribe** button. The button works in one step, and the link asks you to confirm first. Neither needs a login.
71
73
 
72
- 1. Open the post from section 4 in your mail client. It shows an **Unsubscribe** button next to the sender.
74
+ 1. Open the post from section 4 in your mail client. It shows an **Unsubscribe** button next to the sender. If your client shows none, use the **Unsubscribe** link in the email's footer, which asks you to confirm on a page.
73
75
 
74
76
  1. Select it. In the editor, **Subscribers** shows the address as **Unsubscribed**, and later sends leave it out.
75
77
 
@@ -94,13 +96,13 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
94
96
 
95
97
  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
98
 
97
- 1. Filter on `event` equal to `sweep.tick`. A line arrives every minute.
99
+ 1. Filter on `event` equal to `sweep.tick`. A line arrives every minute. If none arrive, the schedule probably isn't running, and deploying again with `--env production` registers it.
98
100
 
99
101
  1. Filter on `event` equal to `send.completed`. Your send from section 4 is there.
100
102
 
101
103
  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
104
 
103
- 1. Filter on `level` equal to `error`. A healthy instance shows nothing.
105
+ 1. Filter on `level` equal to `error`. A healthy instance shows nothing. An occasional `d1.unavailable` line at `warn` is a brief database stall the next run makes up for, and needs nothing. The same line every minute means work isn't getting done, so if you set up alerts, alert on it repeating as well as on `error`.
104
106
 
105
107
  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
108
 
@@ -1,28 +1,22 @@
1
1
  # Go live
2
2
 
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.
3
+ At this point, your instance should be deployed and verified. Before anyone subscribes, [check that AWS granted production access](06-production-access.md#4-check-that-aws-granted-it), so SES can send to anyone. From here, where you go next depends on who's writing the newsletter.
4
4
 
5
5
  ## Start publishing
6
6
 
7
- The rest happens in the editor, at `https://newsletter.example.com/dashboard/`. The Publishing docs walk through it in the order of the dashboard's setup checklist, starting with [Name your publication](../publishing/03-name-your-publication.md).
7
+ Open the editor at `https://newsletter.example.com/dashboard/`. The dashboard's setup steps show what's left, and [Publishing](../publishing/README.md) covers each one.
8
8
 
9
- If someone else is the publisher, let them sign in first:
9
+ ## Hand off to someone else
10
10
 
11
- 1. Add their email address to the policy on the Access application that lets you in, and to `ACCESS_ALLOWED_EMAILS` if you set it, as in [Give the app its two values](03-access.md#4-give-the-app-its-two-values).
11
+ [Let them into the editor](03-access.md#allow-someone-else-in), and add their address beside yours under **Settings → Notifications**, so a problem reaches both of you. Then send them the editor's address and a link to [Publishing](../publishing/README.md). The dashboard's setup steps show them what's left.
12
12
 
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**.
13
+ ## Optional guides
14
14
 
15
- 1. Add their address under **Settings → Notifications** beside yours, so a problem reaches both of you.
15
+ Each one adds one thing, and you can come back to any of them later.
16
16
 
17
- 1. Send them the editor's address and a link to [Name your publication](../publishing/03-name-your-publication.md).
18
-
19
- ## Optional next steps
20
-
21
- Each of these guides adds one thing, and you can come back to any of them later.
22
-
23
- - [Allow Claude to connect](../guides/01-allow-claude.md), so the publisher can connect Claude Desktop, claude.ai, or Claude Code, and have it draft, proofread, and schedule posts as them, through the same API the editor uses.
24
- - [Automate with a service token](../guides/02-automate.md), so a script can call the API with a service token, when no one is signed in.
25
- - [Notifications through Cloudflare's email](../guides/03-notifications.md), so problems are reported even when the email provider has stopped sending.
26
- - [Rate-limit the subscribe form](../guides/05-rate-limit.md), before the subscribe link is shared widely.
27
- - [As your list grows](../guides/09-as-your-list-grows.md), from about 1,000 subscribers.
28
- - [Upgrade to a new release](../guides/07-upgrade.md), when one comes out.
17
+ - Before you share your subscribe link: [Rate-limit the subscribe form](../guides/05-rate-limit.md)
18
+ - To hear about a problem even when SES has stopped sending: [Notifications through Cloudflare's email](../guides/03-notifications.md)
19
+ - To have Claude draft, proofread, and schedule posts: [Allow Claude to connect](../guides/01-allow-claude.md)
20
+ - To run scripts with no one signed in: [Automate with a service token](../guides/02-automate.md)
21
+ - To see when your list needs Workers Paid or higher SES limits: [As your list grows](../guides/09-as-your-list-grows.md)
22
+ - To move to a new release: [Upgrade to a new release](../guides/07-upgrade.md)
@@ -43,7 +43,7 @@ Managed OAuth lets an app such as Claude act with a person's account, the way th
43
43
  curl -si https://newsletter.example.com/api/whoami
44
44
  ```
45
45
 
46
- You should see `401`, with a `www-authenticate` line that starts with `Bearer`. Access can take a few minutes more to start accepting sign-ins. Until it does, signing in shows `invalid_target`; wait, then try again.
46
+ You should see `401`, with a `www-authenticate` line that starts with `Bearer`. Access can take a few minutes more to start accepting sign-ins. Until it does, signing in shows `invalid_target` (`OAuth not enabled for the targeted resource`); wait, then try again.
47
47
 
48
48
  ## 2. Connect Claude, or send the publisher the address
49
49
 
@@ -65,4 +65,9 @@ Anyone the Access application's Allow policy lets into the dashboard can connect
65
65
 
66
66
  1. Ask Claude what the publication is called. It answers with the name the dashboard shows.
67
67
 
68
- If Claude can't connect, [Claude can't connect](../publishing/06-troubleshooting.md#claude-cant-connect) lists what it reports and the fix.
68
+ If a check fails:
69
+
70
+ - **Claude can't connect, or the sign-in fails.** Check section 1's settings, and that the connector's address is the instance's address followed by `/api/mcp`.
71
+ - `kestrel login` says the instance "doesn't offer a sign-in for programs". Managed OAuth is off. Turn it on, as section 1 describes.
72
+ - `kestrel login` or `kestrel api` reports a `401`. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the person's address.
73
+ - **The sign-in says Access didn't say who the person is.** The same goes for a `401` naming `access_unidentified`. The app accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Remove or change any other kind.
@@ -19,7 +19,7 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
19
19
 
20
20
  1. Add `newsletter.example.com` to Email Routing. Cloudflare adds subdomains from your domain's own settings: select `example.com`, then **Settings**, and add `newsletter` under **Subdomains**.
21
21
 
22
- 1. Cloudflare adds MX, SPF, and DKIM records for `newsletter.example.com`, and locks them. Wait until they show as added.
22
+ 1. Cloudflare adds the DNS records it needs for `newsletter.example.com`. Wait until the new row reads **Status: Enabled** and **DNS records: Locked**.
23
23
 
24
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
 
@@ -27,27 +27,25 @@ Avoid turning on Email Routing for a domain that already receives mail elsewhere
27
27
 
28
28
  Cloudflare's email delivers only to addresses you've verified with it. Sending to a verified address is free on any plan. Notifications can go to several addresses, such as the publisher's and yours, and each one needs verifying.
29
29
 
30
- 1. Under **Email Routing**, go to **Destination Addresses**, and add each address saved for notifications.
30
+ 1. Under **Email Routing**, go to **Destination addresses**, and add each address saved for notifications that isn't already verified there.
31
31
 
32
- 1. Open the email Cloudflare sends to each address, and select **Verify email address**. If it's the publisher's address, ask them to do this step.
32
+ 1. Open the email Cloudflare sends to each one, and select **Verify email address**. If it's the publisher's address, ask them to do this step.
33
33
 
34
- A verified address belongs to your Cloudflare account, so every environment in it can use it.
34
+ A verified address belongs to your Cloudflare account, so every environment in it can use it. If the publisher adds or changes a notifications address later, verify the new one too.
35
35
 
36
36
  ## 3. Add the binding and deploy
37
37
 
38
38
  A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email. Without it, they go through your provider.
39
39
 
40
- 1. In `wrangler.jsonc`, add the binding to the `production` block:
40
+ 1. In `wrangler.jsonc`, add the binding to `env.production`, beside `r2_buckets` and not inside `vars`:
41
41
 
42
42
  ```jsonc
43
- "send_email": [
44
- { "name": "NOTIFY", "allowed_destination_addresses": ["you@example.com"] } // ← the notifications addresses
45
- ],
43
+ "send_email": [{ "name": "NOTIFY" }],
46
44
  ```
47
45
 
48
- `allowed_destination_addresses` is optional. It limits the binding to the addresses you list, so a changed setting in the editor can't send notifications anywhere else. Leave it out to allow any address you've verified. If the publisher adds or changes a notifications address later, verify the new one and add it here too.
46
+ Give the binding only its name. This file is committed to your repository, so keep the notifications addresses out of it.
49
47
 
50
- 1. **(Optional)** Notifications come from `Kestrel <kestrel@newsletter.example.com>`. To use another address on that hostname, set `NOTIFY_FROM` in the block's `vars`:
48
+ 1. **(Optional)** Notifications come from `Kestrel <kestrel@newsletter.example.com>`. To use another address on that hostname, set `NOTIFY_FROM` in `env.production.vars`:
51
49
 
52
50
  ```jsonc
53
51
  "NOTIFY_FROM": "Kestrel <alerts@newsletter.example.com>",
@@ -62,7 +60,8 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
62
60
  1. Commit the change, and push it:
63
61
 
64
62
  ```bash
65
- git commit -am "Send notifications through Cloudflare's email"
63
+ git add wrangler.jsonc
64
+ git commit -m "Send notifications through Cloudflare's email"
66
65
  git push
67
66
  ```
68
67
 
@@ -72,17 +71,22 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
72
71
  npx wrangler deploy --env production
73
72
  ```
74
73
 
74
+ The output lists the binding as `env.NOTIFY (unrestricted)`. Cloudflare still delivers only to the addresses you verified.
75
+
75
76
  ## Check it
76
77
 
77
78
  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
79
 
79
- 1. Select **Send a test notification**. It arrives at each notifications address, and **Last notification** shows it as delivered.
80
+ 1. Select **Send a test notification**. **Last notification** shows it as delivered.
81
+
82
+ 1. Check that the test arrives at each notifications address. If it's in the spam folder, mark it **Not spam**.
80
83
 
81
- If the test isn't delivered to an address, **Last notification** shows Cloudflare's reason, such as an address that isn't verified, after the address when there are several. The other addresses still get it. Cloudflare's Email Routing summary lists notifications as dropped even when they're delivered, so check the notifications inbox rather than the summary.
84
+ 1. In Email Routing's **Activity log**, the test shows as **Delivered**. Go by this rather than Email Routing's summary, which [Cloudflare says](https://developers.cloudflare.com/email-service/platform/limits/) can list it as dropped even when it's delivered.
82
85
 
83
86
  ## What notifications don't cover
84
87
 
85
88
  - **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.
89
+ - **The database being down.** Noticing a problem and telling you both need the database. [Read the logs](../get-started/07-verify.md#7-read-the-logs) to check for `d1.unavailable` repeating.
86
90
  - **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
91
  - **Problems while no address is set.** With the address blank, the app sends nothing, and saves nothing up for later.
88
92
 
@@ -16,6 +16,14 @@ A release can ask you to change a setting or a record before it runs. The change
16
16
 
17
17
  Each step here changes only your instance's repository. Nothing reaches your deployed app until section 5.
18
18
 
19
+ 1. Start from a clean repository, so the upgrade's commit holds only the upgrade and going back undoes nothing else:
20
+
21
+ ```bash
22
+ git status
23
+ ```
24
+
25
+ If it lists anything, commit it first, as its own commit, or move it out of the repository.
26
+
19
27
  1. Install the release. Replace `X.Y.Z` with its version:
20
28
 
21
29
  ```bash
@@ -24,6 +32,12 @@ Each step here changes only your instance's repository. Nothing reaches your dep
24
32
 
25
33
  Your `package.json` now names exactly that version. Kestrel's versions aren't semantic versioning, so your instance never moves to a release you didn't choose.
26
34
 
35
+ If npm stops with `ERESOLVE` and names wrangler, your `package.json` pins a wrangler older than the release needs. The error's `peer wrangler@` line gives the range it needs. Install both together, with that range in place of `WRANGLER_RANGE`:
36
+
37
+ ```bash
38
+ npm install @kurtbruns/kestrel@X.Y.Z "wrangler@WRANGLER_RANGE"
39
+ ```
40
+
27
41
  1. Bring in the release's database migrations:
28
42
 
29
43
  ```bash
@@ -91,7 +105,7 @@ A release changes the database only by adding a migration. Applying it keeps eve
91
105
  npx wrangler d1 migrations apply DB --remote --env production
92
106
  ```
93
107
 
94
- It lists the migrations it's about to apply, and asks you to confirm. When a release has none, it says there's nothing to apply. Run it on every upgrade.
108
+ It lists the migrations it's about to apply, and asks you to confirm. Run by Claude, or in any other shell that isn't interactive, it answers its own prompt and applies them without asking. When a release has none, it says there's nothing to apply. Run it on every upgrade.
95
109
 
96
110
  ## 5. Deploy
97
111
 
@@ -99,7 +113,7 @@ It lists the migrations it's about to apply, and asks you to confirm. When a rel
99
113
  npx wrangler deploy --env production
100
114
  ```
101
115
 
102
- The output ends with your app's hostname.
116
+ Near its end, the output names your app's hostname, then its schedule and a version ID.
103
117
 
104
118
  ## Check it
105
119
 
@@ -6,6 +6,14 @@ All three are optional, and you can change them at any time. How they appear in
6
6
 
7
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
8
 
9
+ ## Choose a logo
10
+
11
+ **Use a square PNG, JPEG, WebP, or GIF, at least 256 × 256 pixels, on a solid background.** Drop it on the logo under **Settings → Publication identity**, or choose **Upload**, which reads **Replace** once you have a logo. It saves right away, without **Save changes**. If that would change scheduled emails, Kestrel asks you to confirm first.
12
+
13
+ Kestrel makes a 256 × 256 PNG from your image before uploading it, so the original's file size doesn't matter. A wider or taller image is cropped to its center square, and a smaller one is scaled up and looks soft. An animated GIF keeps its first frame. Emails show the logo at 44 × 44 pixels, so a simple mark reads better than fine detail.
14
+
15
+ A transparent background shows whatever is behind the logo, and the built-in templates turn dark when a reader's mail app is in dark mode. A solid background keeps a dark mark visible there.
16
+
9
17
  ## When to provide a mailing address
10
18
 
11
19
  **If your newsletter is mostly your own writing, you probably don't need one. If an email is mainly selling something, add one.**
@@ -6,11 +6,11 @@ Write in Markdown, schedule it, then send yourself a test. Scheduling makes the
6
6
 
7
7
  ## Write
8
8
 
9
- The editor takes Markdown, and Kestrel saves as you type. The same Markdown becomes the email, in your [email template](11-email-template.md), and the post's page in your archive. **Preview** shows the email.
9
+ The editor takes Markdown, and Kestrel saves as you type. The same Markdown becomes the email, in your [email template](11-email-template.md), and the post's page in your archive. **Preview** shows the email, and warns under it about anything it found wrong.
10
10
 
11
11
  - A blank line starts a new paragraph.
12
12
  - The opening lines are the preview a mail client shows beside the subject.
13
- - Paste or drop an image to add it: PNG, JPEG, WebP, or GIF, up to 5 MB.
13
+ - Paste or drop an image to add it: PNG, JPEG, WebP, or GIF, up to 5 MB. A file named for a different format than it really is, like a JPEG called `photo.png`, is refused until you rename it. Kestrel may change its file name, turning spaces into hyphens and numbering a name the post already has, and inserts the new name for you.
14
14
 
15
15
  ## Schedule and send
16
16
 
@@ -1,18 +1,34 @@
1
1
  # Troubleshooting
2
2
 
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
-
5
- **Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**: type an address and choose **Add inbox**. Add the address of whoever runs your instance too, and both of you are told. With none set, Kestrel emails you nothing. [Notifications](06-troubleshooting.md#notifications), below, lists what you're told.
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 and what to do. This page covers what the screen can't: the fixes made outside Kestrel, and the decisions that are yours.
4
+
5
+ **Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**: type an address and choose **Add inbox**. Add the address of whoever runs your instance too, and both of you are told. With none set, Kestrel emails you nothing. [Notifications](#notifications), below, lists what you're told.
6
+
7
+ | Symptom | Where the fix is |
8
+ | --- | --- |
9
+ | [A send is held or waiting](#a-send-is-held-or-waiting) | Usually outside Kestrel |
10
+ | [Someone was mailed twice, or not at all](#someone-was-mailed-twice-or-not-at-all) | In Kestrel |
11
+ | [A reader says they never got a post](#a-reader-says-they-never-got-a-post) | In Kestrel |
12
+ | [A send still reads Settling](#a-send-still-reads-settling) | Nothing to fix, unless no receipts come at all |
13
+ | [A test doesn't arrive](#a-test-doesnt-arrive) | Outside Kestrel |
14
+ | [Signups aren't open yet, or are paused](#signups-arent-open-yet-or-are-paused) | In Kestrel, or outside it |
15
+ | [The dashboard names a problem with your Amazon SES account](#a-problem-with-amazon-ses) | Outside Kestrel |
16
+ | [An address bounced or complained](#an-address-bounced-or-complained) | Nothing to fix, unless you lift a suppression |
17
+ | [A warning says to move to Workers Paid](../guides/09-as-your-list-grows.md) | Outside Kestrel |
18
+ | [An import is refused](#an-import-is-refused) | In your file |
19
+ | [Claude can't connect](#claude-cant-connect) | Usually outside Kestrel |
6
20
 
7
21
  ## A send is held or waiting
8
22
 
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:
23
+ 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, it's one of these:
10
24
 
11
25
  - **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
26
  - **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
27
  - **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
28
  - **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.
15
29
 
30
+ If someone else set up Kestrel for you, pass this along to them.
31
+
16
32
  ## Someone was mailed twice, or not at all
17
33
 
18
34
  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,24 +43,26 @@ Under **Subscribers**, search for their address and open their row. Their page l
27
43
 
28
44
  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.
29
45
 
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.
46
+ 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. If someone else set up Kestrel for you, pass this along to them.
31
47
 
32
48
  ## A test doesn't arrive
33
49
 
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).
50
+ 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](#a-send-is-held-or-waiting). If someone else set up Kestrel for you, pass this along to them.
35
51
 
36
52
  ## Signups aren't open yet, or are paused
37
53
 
38
54
  Your subscribe page takes signups only while email is working, so a reader is never told a link is coming when none can go. Every visitor gets the same page, whatever address they enter.
39
55
 
40
56
  - "Signups aren’t open yet" means SES hasn't sent an email for your publication yet. Send one from **Template → Send test email**, to an address verified in SES while it's in the sandbox. Once SES takes it, signups open. Before an email provider is connected, the page says the same, and [Connect Amazon SES](../get-started/05-ses.md) is the fix.
41
- - "Signups are paused" means SES is refusing your email, every email including confirmations and tests. The dashboard's **Email isn’t going out** line gives SES's words and the fix, which whoever runs your instance makes, as for [a problem with Amazon SES](#a-problem-with-amazon-ses). Then send a test email: once SES takes one, signups reopen.
57
+ - "Signups are paused" means SES is refusing your email, every email including confirmations and tests. The dashboard's **Email isn’t going out** line gives SES's words and the fix, as for [a problem with Amazon SES](#a-problem-with-amazon-ses). Then send a test email: once SES takes one, signups reopen.
42
58
 
43
59
  Kestrel notices a refusal when an email meets it, or within about ten minutes from its check of your SES account. A refusal the check found lifts on its own within about ten minutes of the fix; one an email met lifts when a test email goes through. A reader who signed up just as it started may have been told a link was coming, and can sign up again once signups reopen.
44
60
 
61
+ If someone else set up Kestrel for you, pass this along to them.
62
+
45
63
  ## A problem with Amazon SES
46
64
 
47
- **Settings → Email sender** and the top of the dashboard name a problem with your Amazon SES account within about ten minutes, without sending anything. Red means no email can go out until it's fixed, tests and confirmations included. Amber is worth a look. Each line links the step that fixes it, and whoever runs your instance makes the fix:
65
+ **Settings → Email sender** and the top of the dashboard name a problem with your Amazon SES account within about ten minutes, without sending anything. Red means no email can go out until it's fixed, tests and confirmations included. Amber is worth a look. Each line links the step that fixes it:
48
66
 
49
67
  - **The configuration set is missing, or its sending is off.** The set Kestrel sends with, the one `SES_CONFIGURATION_SET` names or the sending identity's default if that secret isn't set, isn't in your region, or its sending is turned off. Correct the name, or create the set, as [Connect Amazon SES](../get-started/05-ses.md#4-send-bounces-and-complaints-to-the-app) shows. Turn a set's sending back on in the SES console, under its configuration sets.
50
68
  - **SES rejects the AWS key.** The key ID is unknown, the secret doesn't match it, or the AWS account isn't signed up for SES. Store the key's ID and secret again, or create a new key, as [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) shows.
@@ -54,11 +72,9 @@ Kestrel notices a refusal when an email meets it, or within about ten minutes fr
54
72
 
55
73
  The app needs to read each of these. If its key isn't allowed to, the line says nothing rather than guessing. The policy in [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) allows all of it.
56
74
 
57
- ## Your list outgrows Workers Free
75
+ If someone else set up Kestrel for you, pass this along to them.
58
76
 
59
- 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.
60
-
61
- ## Bounces and complaints
77
+ ## An address bounced or complained
62
78
 
63
79
  Kestrel acts on these for you, so a bad address can't keep hurting your sending reputation:
64
80
 
@@ -78,31 +94,41 @@ Nothing on your list changes. Kestrel refuses a file it can't read with certaint
78
94
 
79
95
  ## Claude can't connect
80
96
 
81
- 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:
97
+ [Connect Claude](08-connect-claude.md) covers connecting it. Most failures are in your instance's Cloudflare Access setup, and [Allow Claude to connect](../guides/01-allow-claude.md#check-it) lists their fixes. When connecting fails:
82
98
 
83
- - **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.
84
- - Signing in shows `invalid_target` (`OAuth not enabled for the targeted resource`). Access can take a few minutes to start accepting sign-ins once Managed OAuth is on. Wait, then try again.
85
- - `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.
86
- - `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.
99
+ - **Claude says it couldn't connect, or the sign-in failed.** 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. If both are right, your instance's Access setup needs the fix.
100
+ - Signing in shows `invalid_target`, or `kestrel login` says your instance "doesn't offer a sign-in for programs". Your instance's Access setup needs the fix.
101
+ - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in, and your instance's Access setup needs the fix. If either names `access_unidentified`, it's the "Access let you in" problem below. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
87
102
  - `kestrel api` says it couldn't renew your sign-in because its Access team answered 404 (not found). First check which instance it called: when you don't name one, it prints a line naming it before "the only instance you're signed in to", which may not be the one you meant. Name yours with `--url`. If it is yours, the Access team domain was most likely renamed or removed since you signed in. Sign in again with `npx kestrel login` and your instance's address, which finds the team it uses now.
88
- - **"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.
103
+ - **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Sign out, then sign in through the policy that lets you in by email. If you have no such policy, your instance's Access setup needs the fix.
89
104
  - **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.
105
+ - **A change reads "via the Kestrel CLI".** The person it names made it with `kestrel api` outside Claude Code, by hand or through a script, so there's nothing to fix. If Claude Code made it, update Claude Code to v2.1.172 or later: an older one reads this way, and the dashboard can stop saying "Claude is connected."
106
+
107
+ If someone else set up Kestrel for you, pass this along to them.
108
+
109
+ ## Where to look
90
110
 
91
- ## Cloudflare won't save the Access application
111
+ When a line doesn't settle it, these say the most about what happened:
92
112
 
93
- 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.
113
+ - The top of the dashboard: what needs someone now, in Kestrel's own words.
114
+ - The send's page, under **Sent**: what happened on that send, when it next retries, and its record of each reader.
115
+ - **Settings → Email sender**: the state of your Amazon SES account, and your **Send rate**.
116
+ - **Settings → Notifications**: **Last notification** says whether the last one was delivered, or why not.
117
+ - A subscriber's page, under **Subscribers**: every send they were in, and what happened on each.
118
+ - The app's logs, in your instance's Cloudflare account: [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes them. If someone else set up Kestrel for you, pass this along to them.
94
119
 
95
120
  ## Notifications
96
121
 
97
122
  With an address set, Kestrel emails you:
98
123
 
99
- - when a send finishes, with how many emails were accepted, unsent, and skipped
100
- - when your provider refuses your account or a send, or no provider is connected
124
+ - when a send finishes, with how many emails your provider accepted, and how late it went out if it did
125
+ - when no one's email was accepted for a finished send, since it can't be sent again
126
+ - when email isn't going out and signups are paused: once when it starts, and once more if a post is held after an email that named none
127
+ - when your provider refuses a send or hits a limit, or no provider is connected
101
128
  - when the SES sandbox refuses a signup's confirmation, at most once a day
102
- - when email isn't going out and signups are paused, found by a reader's signup or the check of your SES account, at most once a day while it lasts
103
- - when a send stops going out with nothing to explain it, waits for you to resolve it, or misses its time
104
- - when a service token imports a list, since no one saw it happen (an import you make yourself, or through Claude, sends nothing)
129
+ - when a send stops going out with nothing to explain it, waits for you to resolve it, or doesn't go out on time
130
+ - when your provider has sent no receipts for six hours after a send, once until a receipt arrives
105
131
 
106
- Each goes to every address set, links to the page it's about, and is sent once while the problem lasts. If one address can't be reached, the others still get it, and **Last notification** names the one that failed. A bounce spike shows on the dashboard but isn't emailed. **Send a test notification** checks that one arrives.
132
+ Each says whether anyone has to act, and how. If the fix needs whoever set up your instance, you can forward it as it stands. Each goes to every address set, links to the page it's about, and is sent once while the problem lasts. A reader's address in your provider's words shows only as its domain, and in full on the send's page. If one address can't be reached, the others still get it, and **Last notification** names the one that failed. A bounce spike shows on the dashboard but isn't emailed. **Send a test notification** checks that one arrives.
107
133
 
108
134
  By default, notifications go through your newsletter's own email provider. So when that provider refuses your account, the notification about it can't reach you either. You can send them another way, as [Notifications through Cloudflare's email](../guides/03-notifications.md) describes.
@@ -26,7 +26,7 @@ A variable is replaced with its value in each email. Type it exactly as shown, o
26
26
  | `{{ .Publication.LogoURL }}` | Your logo's address, or empty. |
27
27
  | `{{ .Publication.Address }}` | Your mailing address, or empty. |
28
28
  | `{{ .Email.SentTo }}` | The address this email was sent to. |
29
- | `{{ .Email.UnsubscribeURL }}` | This reader's one-click unsubscribe link. Required. |
29
+ | `{{ .Email.UnsubscribeURL }}` | This reader's unsubscribe link. Required. |
30
30
  | `{{ .Email.ViewInBrowserURL }}` | The post's own web link, which opens its page once it's sent, in your archive or not. |
31
31
 
32
32
  ## Test it
@@ -4,7 +4,7 @@ These pages get you publishing in the dashboard, and cover the decisions to make
4
4
 
5
5
  ## Set up your publication
6
6
 
7
- These are the dashboard's setup steps, in its order. Each is checked off from what Kestrel can see, which Claude reads too, except sharing your link, which Kestrel can't see. Connecting Amazon SES and requesting production access are for whoever runs your Kestrel instance, in [Connect Amazon SES](../get-started/05-ses.md) and [Request production access](../get-started/06-production-access.md). If you're starting without a list, skip importing one.
7
+ These are the dashboard's setup steps, in its order. Each is checked off from what Kestrel can see, which Claude reads too, except sharing your link, which Kestrel can't see. Connecting Amazon SES and requesting production access are for whoever runs your Kestrel instance, in [Connect Amazon SES](../get-started/05-ses.md) and [Request production access](../get-started/06-production-access.md). If you're starting without a list, skip importing one. Setup ends when your first post goes out to the archive, and an email-only send doesn't count.
8
8
 
9
9
  1. Connect Amazon SES
10
10
  1. [Send a test email](10-check-your-setup.md)
@@ -52,14 +52,14 @@ In Cloudflare, set every `CNAME` for DKIM to **DNS only**, not proxied. SES repo
52
52
 
53
53
  ### DMARC: publish a policy and collect reports
54
54
 
55
- DMARC tells an inbox what to do with mail that fails both SPF and DKIM, and asks it to send you reports. It's a `TXT` record at `_dmarc.send.example.com`:
55
+ DMARC tells an inbox what to do with mail that fails both SPF and DKIM, and can ask it to send you reports. It's a `TXT` record at `_dmarc.send.example.com`:
56
56
 
57
57
  ```
58
58
  _dmarc.send.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
59
59
  ```
60
60
 
61
- - `p=none` is monitor mode. Inboxes report on failing mail, but deliver it as usual.
62
- - `rua` is where reports go. They arrive as attachments, usually one a day from each inbox provider.
61
+ - `p=none` is monitor mode, and it only monitors. Inboxes deliver failing mail as usual, so it doesn't stop anyone sending as your domain. It's where to start because it blocks nothing, your own mail included.
62
+ - `rua` is where reports go. It's optional, and the policy works without it, though [Google's DMARC setup page](https://knowledge.workspace.google.com/admin/security/set-up-dmarc) recommends including it. Reports arrive as attachments, usually one a day from each inbox provider.
63
63
 
64
64
  Leave alignment relaxed, the default, with no `aspf=s` or `adkim=s`. With a custom MAIL FROM domain, your return path is a subdomain of your sending name, so strict SPF alignment would fail.
65
65