@kurtbruns/kestrel 1.10.0 → 1.11.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 +13 -1
  2. package/README.md +19 -29
  3. package/dist/public/dashboard/app.js +1 -1
  4. package/dist/public/dashboard/app.js.map +2 -2
  5. package/dist/public/dashboard/index.html +1 -1
  6. package/dist/worker/index.js +86 -190
  7. package/docs/README.md +2 -2
  8. package/docs/get-started/02-deploy.md +12 -5
  9. package/docs/get-started/03-access.md +2 -0
  10. package/docs/get-started/04-ses.md +14 -14
  11. package/docs/get-started/05-verify.md +19 -7
  12. package/docs/get-started/06-go-live.md +13 -55
  13. package/docs/guides/01-connect-claude.md +4 -4
  14. package/docs/guides/03-notifications.md +10 -10
  15. package/docs/guides/09-as-your-list-grows.md +62 -0
  16. package/docs/publishing/01-your-list.md +118 -47
  17. package/docs/publishing/02-sending-rules.md +5 -5
  18. package/docs/publishing/03-name-your-publication.md +7 -93
  19. package/docs/publishing/04-write-a-post.md +30 -73
  20. package/docs/publishing/06-troubleshooting.md +74 -0
  21. package/docs/publishing/07-working-with-claude.md +18 -37
  22. package/docs/publishing/09-share-your-subscribe-link.md +11 -59
  23. package/docs/publishing/10-check-your-setup.md +12 -30
  24. package/docs/publishing/11-email-template.md +34 -0
  25. package/docs/publishing/README.md +12 -11
  26. package/docs/reference/01-configuration.md +6 -6
  27. package/docs/reference/02-sending-domain-dns.md +1 -1
  28. package/package.json +1 -1
  29. package/template/claude/skills/migrate-list/SKILL.md +2 -2
  30. package/docs/guides/08-try-locally.md +0 -108
  31. package/docs/publishing/05-scheduling-and-sending.md +0 -72
  32. package/docs/publishing/06-after-a-send.md +0 -109
  33. package/docs/publishing/08-import-format.md +0 -103
package/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Self-hosting Kestrel
2
2
 
3
- These pages deploy and run your own instance of Kestrel, from an empty repository to a newsletter on your own Cloudflare account. Writing and sending the newsletter is covered in [Publishing](publishing/README.md).
3
+ These pages cover deploying and running your own Kestrel, from an empty repository to a live newsletter on your Cloudflare account. Writing and sending the newsletter is covered in [Publishing](publishing/README.md). These pages call whoever writes and sends it the publisher, which may be you.
4
4
 
5
5
  ## Get started
6
6
 
@@ -17,9 +17,9 @@ Follow these steps, from an empty repository to a live newsletter. Each one ends
17
17
  Each one stands on its own. Pick the ones you need.
18
18
 
19
19
  - [Connect Claude to the API](guides/01-connect-claude.md)
20
- - [Try Kestrel locally first](guides/08-try-locally.md)
21
20
  - [Notifications through Cloudflare's email](guides/03-notifications.md)
22
21
  - [Rate-limit the subscribe form](guides/05-rate-limit.md)
22
+ - [As your list grows](guides/09-as-your-list-grows.md)
23
23
  - [Upgrade to a new release](guides/07-upgrade.md)
24
24
 
25
25
  ## Reference
@@ -56,7 +56,14 @@ Your instance's repository holds its configuration, its database migrations, and
56
56
  git push -u origin HEAD
57
57
  ```
58
58
 
59
- 1. **(Optional)** Try Kestrel locally before you deploy it. Your instance is written and installed, so start at [Start the app](../guides/08-try-locally.md#2-start-the-app), and come back here when you're done. Email goes to a stand-in, so nothing reaches a real inbox.
59
+ 1. **(Optional)** Try Kestrel locally before you deploy it. Copy the local settings, and start the app:
60
+
61
+ ```bash
62
+ cp .dev.vars.example .dev.vars
63
+ npm run dev
64
+ ```
65
+
66
+ Open http://localhost:8787/dashboard/, and run `npm run seed` in a second terminal to load a demo publication. Email goes to a stand-in, so nothing reaches a real inbox. Stop the server with <kbd>Ctrl</kbd>+<kbd>C</kbd> when you're done.
60
67
 
61
68
  ## 2. Sign in to Cloudflare
62
69
 
@@ -118,7 +125,7 @@ Name `--env production` on every Wrangler command that creates or binds a resour
118
125
  }
119
126
  ],
120
127
  "vars": {
121
- "PROVIDER": "", // ← stays empty until step 3
128
+ "PROVIDER": "", // ← stays empty until Connect Amazon SES
122
129
  "APP_ORIGIN": "https://newsletter.example.com", // ← your app's hostname
123
130
  "ARCHIVE_BASE_PATH": "/archive",
124
131
  "SENDING_DOMAIN": "send.example.com", // ← your sending hostname
@@ -130,8 +137,8 @@ Name `--env production` on every Wrangler command that creates or binds a resour
130
137
  What each one does:
131
138
 
132
139
  - **`routes`** puts the app on your hostname when you deploy. Cloudflare creates the DNS record and the certificate. Your domain must be in the same Cloudflare account, and the hostname can't already have a CNAME record; delete one if it does. The block also turns off the `workers.dev` addresses, so the app answers only on your hostname.
133
- - **`PROVIDER` stays empty.** Step 3, [Connect Amazon SES](04-ses.md), sets it. Until then the app runs without an email provider: you can write and preview posts, but scheduling, test emails, and signups are refused. Never set it to `fake`, the top level's value, even when a wrangler warning suggests copying it: `fake` delivers nothing, and the app refuses it on your hostname.
134
- - **`ARCHIVE_BASE_PATH`** is permanent once you send. Every post you send carries its `/archive/…` link, and changing the prefix later breaks the links you've already mailed. Keep `/archive` unless you have a reason not to.
140
+ - **`PROVIDER` stays empty.** [Connect Amazon SES](04-ses.md) sets it. Until then the app runs without an email provider: posts can be written and previewed, but scheduling, test emails, and signups are refused. Never set it to `fake`, the top level's value, even when a wrangler warning suggests copying it: `fake` delivers nothing, and the app refuses it on your hostname.
141
+ - **`ARCHIVE_BASE_PATH`** is permanent once the first post goes out. Every post sent carries its `/archive/…` link, and changing the prefix later breaks the links already mailed. Keep `/archive` unless you have a reason not to.
135
142
  - **`FROM_ADDRESS`** is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`.
136
143
 
137
144
  Change every `example.com` value, the route included, even before you connect a provider. The app checks its settings but can't see the route: it refuses to run while `APP_ORIGIN`, `SENDING_DOMAIN`, or `FROM_ADDRESS`, or `ARCHIVE_ORIGIN` or `MEDIA_PUBLIC_BASE` if you set them, is still on `example.com`, since its links and the address your email comes from would name a domain that isn't yours.
@@ -195,7 +202,7 @@ This deploys the release your instance runs, with your settings. The output ends
195
202
 
196
203
  In a browser, `https://newsletter.example.com/dashboard/` says the editor is locked until you set up Access. That's expected.
197
204
 
198
- 1. In a browser, `https://newsletter.example.com/` shows your newsletter's public landing page. Its subscribe page, `/subscribe`, says signups aren't open yet, until step 3, Connect Amazon SES.
205
+ 1. In a browser, `https://newsletter.example.com/` shows the newsletter's public landing page. Its subscribe page, `/subscribe`, says signups aren't open yet, until Connect Amazon SES.
199
206
 
200
207
  If the public pages instead say the newsletter is temporarily unavailable, and `/health` answers `500` with a body like `{"error":"invalid_config","variable":"APP_ORIGIN",…}`, the setting it names is missing, malformed, or still on `example.com`. Fix it in `wrangler.jsonc`, commit, and deploy again.
201
208
 
@@ -37,6 +37,8 @@ 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 landing page, the archive, subscribing, unsubscribing, images, and your email provider's webhooks must stay public.
39
39
 
40
+ 1. **(Optional)** Choose how long a sign-in lasts. The application's session duration defaults to 24 hours, so everyone signs in again each day. You can set anything from immediately to one month, as Cloudflare's [session management](https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/session-management/) describes. A longer session suits a publisher on their own computer, and a shorter one is safer on a shared one.
41
+
40
42
  ## 3. Allow yourself in
41
43
 
42
44
  Every Access application denies everyone until a policy allows them.
@@ -1,8 +1,8 @@
1
1
  # Connect Amazon SES
2
2
 
3
- Kestrel sends your posts through Amazon SES, Amazon's email service. SES only delivers the mail: Kestrel keeps your list, each subscriber's consent, and the record of every send. AWS reviews your account before it lets you send to anyone but yourself. This page asks for that early, and you test in the sandbox while you wait.
3
+ Kestrel sends the newsletter's posts through Amazon SES, Amazon's email service. SES only delivers the mail: Kestrel keeps the list, each subscriber's consent, and the record of every send. AWS reviews your account before it lets you send to anyone but yourself. This page asks for that early, and you test in the sandbox while you wait.
4
4
 
5
- In this guide, you verify your sending hostname with SES and add a DMARC record. You forward readers' replies to your inbox, and ask AWS for production access. You also route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, set the provider, and deploy.
5
+ In this guide, you verify your sending hostname with SES and add a DMARC record. You forward readers' replies to the publisher's inbox, and ask AWS for production access. You also route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, set the provider, and deploy.
6
6
 
7
7
  ## Before you begin
8
8
 
@@ -44,7 +44,7 @@ DMARC tells inboxes what to do with mail that fails its checks, and sends you re
44
44
 
45
45
  ## 3. Receive replies at your From address
46
46
 
47
- When a reader replies to a post, or to their confirmation email, the reply goes to your From address, `newsletter@send.example.com`. Nothing receives mail there yet, so the reply bounces. Kestrel only sends mail, so you give the address an inbox yourself. Here you forward it to one you already read, with Cloudflare Email Routing, which is free on every plan.
47
+ When a reader replies to a post, or to their confirmation email, the reply goes to your From address, `newsletter@send.example.com`. Nothing receives mail there yet, so the reply bounces. Kestrel only sends mail, so you give the address an inbox yourself. Here you forward it to the publisher's inbox, with Cloudflare Email Routing, which is free on every plan. The publisher is whoever writes and sends the newsletter, which may be you.
48
48
 
49
49
  `send.example.com` receives no other mail, so turning on Email Routing there changes nothing else. Your SES records sit on other names under it. Inboxes check SES's mail for SPF at its return path, not here, so sending is unaffected.
50
50
 
@@ -54,19 +54,19 @@ When a reader replies to a post, or to their confirmation email, the reply goes
54
54
 
55
55
  1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
56
56
 
57
- 1. Under **Destination Addresses**, add the inbox you want replies in. Open the email Cloudflare sends to it, and select **Verify email address**. An address you've already verified with Cloudflare, such as one for [notifications](../guides/03-notifications.md), is listed already.
57
+ 1. Under **Destination Addresses**, add the publisher's inbox. Open the email Cloudflare sends to it, and select **Verify email address**. If it isn't your inbox, ask the publisher to do this step. An address you've already verified with Cloudflare, such as one for [notifications](../guides/03-notifications.md), is listed already.
58
58
 
59
- 1. Under **Routing Rules**, select **Create routing rule**. For **Email pattern**, enter `newsletter`, the part of your From address before the `@`, and select `send.example.com`. Set **Action** to **Send to an email**, select your inbox as the **Destination**, and save.
59
+ 1. Under **Routing Rules**, select **Create routing rule**. For **Email pattern**, enter `newsletter`, the part of your From address before the `@`, and select `send.example.com`. Set **Action** to **Send to an email**, select the publisher's inbox as the **Destination**, and save.
60
60
 
61
61
  On a subdomain, Email Routing forwards only the addresses you add a rule for, with no catch-all. That's all a From address needs.
62
62
 
63
- - **Automatic replies arrive too.** Out-of-office and other automatic replies go to your From address, so they reach your inbox. Bounces don't: they go to SES's return path, and the app handles them as before.
63
+ - **Automatic replies arrive too.** Out-of-office and other automatic replies go to your From address, so they reach the publisher's inbox. Bounces don't: they go to SES's return path, and the app handles them as before.
64
64
  - **Your DMARC reports need an inbox too.** Your DMARC record's report address may not receive mail. If so, change the record's `rua` to `mailto:dmarc@send.example.com`, and add a second rule that forwards `dmarc` the same way.
65
- - **Your answers come from your inbox.** A forwarded reply comes from the reader's own address. When you answer it, your answer goes out from the inbox it was forwarded to, not from `newsletter@send.example.com`.
65
+ - **Answers come from the publisher's inbox.** A forwarded reply comes from the reader's own address. An answer to it goes out from the inbox it was forwarded to, not from `newsletter@send.example.com`.
66
66
 
67
67
  To answer from the newsletter's own address, give it a real mailbox instead of forwarding it. Point `send.example.com`'s MX records at a mail host you use, such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034). Then create `newsletter@send.example.com` there, as the host's docs describe. If you send from it, the host's SPF and DKIM records go on `send.example.com` beside SES's. SES's mail is unaffected, since its SPF is checked at its return path. And if your From address is on a domain that already receives mail, replies already reach it, and you can skip this section.
68
68
 
69
- Some replies ask you to do something, and Kestrel can't see them, so acting on them is yours. In the editor, open **Subscribers**, and use the reader's row menu. Choose **Unsubscribe** for a reader who asks to leave, and **Erase** for one who asks you to delete their data. A reader who never confirmed isn't mailed, so their row offers only **Erase**.
69
+ Some replies ask the publisher to do something, and Kestrel can't see them, so the publisher acts on them, as [Sending rules](../publishing/02-sending-rules.md#when-a-reader-asks-to-leave-or-to-be-deleted) describes.
70
70
 
71
71
  ## 4. (Optional) Set a custom MAIL FROM domain
72
72
 
@@ -87,7 +87,7 @@ SES sends with its own return path by default, so SPF passes for `amazonses.com`
87
87
 
88
88
  A new SES account starts in the sandbox. It sends only to addresses you've verified in SES, up to 200 emails a day and one a second. Ask AWS to lift those limits now, and test while you wait.
89
89
 
90
- 1. Name your publication first, since AWS may visit your website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline. Until you do, your public pages show the name in your From address. The subscribe form stays closed until section 8 sets the provider.
90
+ 1. Name the publication first, or ask the publisher for its name, since AWS may visit its website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline. Until then, the public pages show the name in your From address. The subscribe form stays closed until section 8 sets the provider.
91
91
 
92
92
  1. In SES, open the **Account dashboard**, select **View Get set up page**, and then **Request production access**.
93
93
 
@@ -100,16 +100,16 @@ A new SES account starts in the sandbox. It sends only to addresses you've verif
100
100
 
101
101
  1. While you wait, verify the two addresses you test with in [Verify it works](05-verify.md#before-you-begin), so the sandbox lets you mail them. For each, under **Identities**, choose **Create identity**, select **Email address**, and follow the link in the email AWS sends.
102
102
 
103
- If AWS asks for more detail, reply in the support case. If it denies the request, reply there, or open a new request from the **Account dashboard**. Either way, say how Kestrel handles your list:
103
+ If AWS asks for more detail, reply in the support case. If it denies the request, reply there, or open a new request from the **Account dashboard**. Either way, say how Kestrel handles the list:
104
104
 
105
105
  - Signups use double opt-in: each subscriber confirms by email before joining the list.
106
106
  - Every post offers one-click unsubscribe.
107
107
  - Bounces and complaints reach the app through SNS, and it stops mailing those addresses automatically.
108
- - How many subscribers you expect, and how often you send.
108
+ - How many subscribers the newsletter expects, and how often it sends.
109
109
 
110
110
  Your account stays in the sandbox until AWS grants access, so don't go live before then. In the sandbox, a signup from any address you haven't verified fails, and a post reaches only your verified addresses.
111
111
 
112
- When AWS grants access, the **Account dashboard** shows your daily quota and your maximum send rate. You set the rate in the app in section 8. The rate decides how fast a send goes out, and [As your list grows](06-go-live.md#as-your-list-grows) says when to ask AWS to raise it.
112
+ When AWS grants access, the **Account dashboard** shows your daily quota and your maximum send rate. You set the rate in the app in section 8. The rate decides how fast a send goes out, and [As your list grows](../guides/09-as-your-list-grows.md) says when to ask AWS to raise it.
113
113
 
114
114
  ## 6. Send bounces and complaints to SNS
115
115
 
@@ -223,7 +223,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
223
223
  }
224
224
  ```
225
225
 
226
- On Workers Paid, also add `"SUBREQUEST_BUDGET": "10000"`, the plan's limit, so each minute sends more. Leave it out on Workers Free. [As your list grows](06-go-live.md#as-your-list-grows) says when the plan is worth it.
226
+ On Workers Paid, also add `"SUBREQUEST_BUDGET": "10000"`, the plan's limit, so each minute sends more. Leave it out on Workers Free. [As your list grows](../guides/09-as-your-list-grows.md) says when the plan is worth it.
227
227
 
228
228
  When AWS grants production access, set `SES_MAX_SEND_RATE` to the maximum send rate on the **Account dashboard**, and deploy again. The app keeps to that rate, so SES doesn't throttle your send.
229
229
 
@@ -310,7 +310,7 @@ Kestrel can't tell whether those emails went out. SES's console shows sent messa
310
310
  dig +short MX send.example.com
311
311
  ```
312
312
 
313
- It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows your inbox as verified. A reply to your test email in the next step proves the forwarding works.
313
+ It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows the publisher's inbox as verified. A reply to your test email in the next step proves the forwarding works.
314
314
 
315
315
  1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before.
316
316
 
@@ -1,8 +1,8 @@
1
1
  # Verify it works
2
2
 
3
- Some things only show up with real email: whether inboxes trust your mail, how it looks in a real mail client, and whether bounces and unsubscribes come back to the app. Check them now, while your list is empty and every email goes only to you.
3
+ Some things only show up with real email: whether inboxes trust your mail, how it looks in a real mail client, and whether bounces and unsubscribes come back to the app. Check them now, while the list is empty and every email goes only to you.
4
4
 
5
- In this guide, you send yourself a test email, reply to it, and check that it passes authentication. Then you set up your notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, and read the app's logs.
5
+ In this guide, you send yourself a test email, reply to it, and check that it passes authentication. Then you set up notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, read the app's logs, and check that SES can send to anyone.
6
6
 
7
7
  ## Before you begin
8
8
 
@@ -19,7 +19,7 @@ A test email goes through the same code as a real send. If the test looks right,
19
19
 
20
20
  1. Open the email. It should arrive in your inbox, not in spam. The image loads and the subject reads as you wrote it. Its **View in browser** link opens "Nothing here yet" until the post goes out, and its unsubscribe link opens the page a subscriber sees, marked as a test and showing a sample address. Its button walks through to the confirmation, but a test email unsubscribes no one. Section 5 unsubscribes for real.
21
21
 
22
- 1. Reply to it, as a reader would. Reply from an address other than the inbox your From address forwards to, such as your second address. Gmail doesn't show you a message you sent when it comes back to you. Send that address a test email too if you need one. The reply arrives in the inbox you chose in [Connect Amazon SES](04-ses.md#3-receive-replies-at-your-from-address). The app can't see your forwarding, so this is the only proof that readers can reach you. If the reply bounces, check the routing rule and the destination address there.
22
+ 1. Reply to it, as a reader would. Reply from an address other than the inbox your From address forwards to, such as your second address. Gmail doesn't show you a message you sent when it comes back to you. Send that address a test email too if you need one. The reply arrives in the inbox you chose in [Connect Amazon SES](04-ses.md#3-receive-replies-at-your-from-address). The app can't see your forwarding, so this is the only proof that readers can reach the publisher. If the reply bounces, check the routing rule and the destination address there.
23
23
 
24
24
  ## 2. Check that it passes authentication
25
25
 
@@ -37,9 +37,9 @@ Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing
37
37
 
38
38
  If a check fails, compare your DNS records with the ones in [Connect Amazon SES](04-ses.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
39
39
 
40
- ## 3. Set up your notifications
40
+ ## 3. Set up notifications
41
41
 
42
- Kestrel emails you when a send finishes, and right away when one runs into a problem. Notifications go through your email provider. To send them through Cloudflare instead, see [Notifications through Cloudflare's email](../guides/03-notifications.md).
42
+ Kestrel emails the notifications address when a send finishes, and right away when one runs into a problem. Notifications go through your email provider. To send them through Cloudflare instead, see [Notifications through Cloudflare's email](../guides/03-notifications.md).
43
43
 
44
44
  1. In the editor, open **Settings → Notifications**.
45
45
 
@@ -47,6 +47,8 @@ Kestrel emails you when a send finishes, and right away when one runs into a pro
47
47
 
48
48
  1. Select **Send a test notification**. It arrives at that address.
49
49
 
50
+ Use your own address to test. If you're not the publisher, they set theirs in [Check your setup](../publishing/10-check-your-setup.md).
51
+
50
52
  ## 4. Send a real post to yourself
51
53
 
52
54
  This is a real send, to a list of one. It proves the whole path: subscribing, confirming, scheduling, and sending.
@@ -55,7 +57,7 @@ This is a real send, to a list of one. It proves the whole path: subscribing, co
55
57
 
56
58
  1. Open the confirmation email, follow its link, and select **Confirm**. In the editor, **Subscribers** shows the address as **Confirmed**.
57
59
 
58
- 1. Open your post, select **Schedule**, and then **Send now**. Every send waits at least five minutes before it goes out, so you can cancel a mistake.
60
+ 1. Open the post, select **Schedule**, and then **Send now**. Every send waits at least five minutes before it goes out, so you can cancel a mistake.
59
61
 
60
62
  1. Cancel the send, to see that canceling stops it. Then select **Send now** again, and let it go.
61
63
 
@@ -86,7 +88,7 @@ If an address has no flag, SES's events aren't reaching the app. Check the confi
86
88
 
87
89
  ## 7. Read the logs
88
90
 
89
- The app writes a log line for each thing it does, and Cloudflare keeps them for a few days. Nothing else tells you if the once-a-minute schedule that sends your posts stops, so it's worth knowing where to look.
91
+ The app writes a log line for each thing it does, and Cloudflare keeps them for a few days. Nothing else tells you if the once-a-minute schedule that sends posts stops, so it's worth knowing where to look.
90
92
 
91
93
  1. In the Cloudflare dashboard, go to **Workers & Pages**, select `kestrel-production`, and select **Observability**.
92
94
 
@@ -96,6 +98,16 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
96
98
 
97
99
  1. Filter on `level` equal to `error`. A healthy instance shows nothing.
98
100
 
101
+ ## 8. Check that SES can send to anyone
102
+
103
+ [Connect Amazon SES](04-ses.md#5-request-production-access) asked AWS for production access. Until AWS grants it, the sandbox mails only your verified addresses, so check it before you go live.
104
+
105
+ 1. In SES, open the **Account dashboard** in the region the app sends from. It no longer says your account is in the sandbox, and it shows your daily quota and maximum send rate.
106
+
107
+ 1. Check that `SES_MAX_SEND_RATE` in `wrangler.jsonc` matches that rate, and that you deployed after setting it, as [Connect Amazon SES](04-ses.md#8-set-the-provider-and-deploy) describes.
108
+
109
+ If the dashboard still says sandbox, AWS hasn't answered yet. Every other section works in the sandbox, so come back to this one when it does.
110
+
99
111
  When every section gives the result it describes, your instance is ready for subscribers.
100
112
 
101
113
  Next, [go live](06-go-live.md).
@@ -1,67 +1,25 @@
1
1
  # Go live
2
2
 
3
- This last step makes the newsletter yours, and tells people where to subscribe.
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.
4
4
 
5
- In this guide, you check your publication's name, add a logo, and share your subscribe link. You also see what to change as your list grows.
5
+ ## Start publishing
6
6
 
7
- ## Before you begin
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). The checklist itself shows only until the first post exists, so the test post from [Verify it works](05-verify.md) has already hidden it.
8
8
 
9
- - SES production access, granted in the region the app sends from. [Connect Amazon SES](04-ses.md#5-request-production-access) asks for it.
10
- - `SES_MAX_SEND_RATE` set to the maximum send rate on the SES **Account dashboard**, and deployed. [Connect Amazon SES](04-ses.md#8-set-the-provider-and-deploy) sets it to `1` for the sandbox.
9
+ If someone else is the publisher, let them sign in first:
11
10
 
12
- In the sandbox, SES refuses the confirmation email to any address you haven't verified, so those signups fail. A post reaches only the addresses you verified in SES.
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.
13
12
 
14
- ## 1. Name your publication
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**.
15
14
 
16
- Your publication's name, tagline, and logo appear on the public pages readers see, and in every email. You named it in [Connect Amazon SES](04-ses.md#5-request-production-access). Check those, and add a logo before anyone sees your pages.
15
+ 1. Send them the editor's address and a link to [Name your publication](../publishing/03-name-your-publication.md).
17
16
 
18
- 1. In the editor, open **Settings → Publication identity**.
17
+ ## Optional next steps
19
18
 
20
- 1. Check the name and tagline, and upload a logo.
21
-
22
- 1. **(Optional)** Select **Add a mailing address**. US law asks for a postal address in promotional email, and a P.O. box works. The built-in email template prints it in every email's footer.
23
-
24
- ## 2. Share your subscribe link
25
-
26
- Readers subscribe on a page the app hosts. Every signup confirms by email before it joins your list.
27
-
28
- 1. In the editor, open **Settings → Ways to subscribe**.
29
-
30
- 1. Copy your subscribe page's link, `https://newsletter.example.com/subscribe`, and share it.
31
-
32
- 1. **(Optional)** To take signups on your own website, copy the embeddable subscribe form from the same page, and paste it into your site.
33
-
34
- 1. **(Optional)** Cap how often one visitor can submit the form. The app already sends any one address at most one confirmation every 15 minutes. A Cloudflare rule can also limit each visitor, and the Free plan includes one. See [Rate-limit the subscribe form](../guides/05-rate-limit.md).
35
-
36
- ## As your list grows
37
-
38
- A new instance runs on Cloudflare's free Workers plan. As your list grows, three limits come into play.
39
-
40
- - **Your SES quota.** AWS caps how many emails your account sends a day. A send that hits the cap waits, and the app emails you. It resumes on its own once the quota frees up, or after AWS raises it.
41
- - **How fast a send goes out.** A send goes out a slice at a time, one slice a minute. SES takes one request a second, of up to 50 recipients, and no more recipients a second than your account's maximum send rate. So each request carries one second of that rate, up to 50.
42
- - **Your database's daily writes.** On the free plan, D1, the database the app runs on, allows 100,000 rows written a day. Past that, it refuses every query until midnight UTC, and the whole app stops: sends, signups, the public pages, and the editor.
43
-
44
- It's recommended to move to the $5 Workers Paid plan from about 5,000 subscribers. A send writes about 14 database rows a recipient, delivery receipts included. So one send uses up the free plan's day at about 7,000 recipients, and sooner when an import or anything else writes that day. Workers Paid counts [D1's writes](https://developers.cloudflare.com/d1/platform/pricing/) by the month instead, with 50 million included, and never stops the database for them. On the free plan, the app warns you before a send to more than 5,000 recipients, in the Schedule dialog and on the scheduled post. A smaller send that fires on a day you've imported subscribers gets the same warning on the scheduled post. The app also warns before an import that would use most of a day's writes with the day's earlier imports. It never stops you, since your account may already be on Workers Paid.
45
-
46
- The free Workers plan sends four requests a minute. At 14 messages a second, a common starting rate, that's 56 recipients a minute, and 1,000 subscribers take about 18 minutes. At 50 a second or more, it's 200 a minute.
47
-
48
- To send faster, first ask AWS to raise your maximum send rate to 50, which costs nothing, then set [`SES_MAX_SEND_RATE`](../reference/01-configuration.md#ses_max_send_rate) to match, and deploy again. A rate above 50 adds nothing on the free plan, where a rate of 50 sends 5,000 subscribers in about 25 minutes. A larger list belongs on Workers Paid for its database writes anyway. On Workers Paid, set [`SUBREQUEST_BUDGET`](../reference/01-configuration.md#subrequest_budget) to `10000`. It sends about 50 requests a minute: about 700 recipients at 14 a second, and about 2,500 at 50 or more.
49
-
50
- ## Check it
51
-
52
- 1. The SES **Account dashboard**, in the region the app sends from, no longer says your account is in the sandbox.
53
-
54
- 1. `https://newsletter.example.com/subscribe` shows your publication's name and logo.
55
-
56
- 1. `https://newsletter.example.com/` shows your publication's name, and the post you sent yourself in [Verify it works](05-verify.md#4-send-a-real-post-to-yourself).
57
-
58
- ## What's next
59
-
60
- Each of these guides stands on its own. Pick the ones you want.
19
+ Each of these guides adds one thing, and you can come back to any of them later.
61
20
 
62
21
  - [Connect Claude to the API](../guides/01-connect-claude.md), so Claude can draft, proofread, and schedule posts through the same API the editor uses.
63
- - [Notifications through Cloudflare's email](../guides/03-notifications.md), so you hear about problems even when your email provider has stopped sending.
64
-
65
- If someone else writes the newsletter, point them to the docs' Publishing section, written for them, starting with [Bring your existing list](../publishing/01-your-list.md).
66
-
67
- When a new release comes out, [Upgrade to a new release](../guides/07-upgrade.md) shows how to move to it.
22
+ - [Notifications through Cloudflare's email](../guides/03-notifications.md), so problems are reported even when the email provider has stopped sending.
23
+ - [Rate-limit the subscribe form](../guides/05-rate-limit.md), before the subscribe link is shared widely.
24
+ - [As your list grows](../guides/09-as-your-list-grows.md), from about 5,000 subscribers.
25
+ - [Upgrade to a new release](../guides/07-upgrade.md), when one comes out.
@@ -1,6 +1,6 @@
1
1
  # Connect Claude to the API
2
2
 
3
- Claude Code can draft, edit, proofread, and schedule your posts through the same API the editor uses. It has its own Cloudflare Access credential, so its changes show as Claude's, not yours. Anything it schedules waits out the review window, where you can still cancel it.
3
+ Claude Code can draft, edit, proofread, and schedule posts through the same API the editor uses. It has its own Cloudflare Access credential, so its changes show as Claude's, not a person's. Anything it schedules waits out the review window, where the publisher can still cancel it.
4
4
 
5
5
  In this guide, you create a service token for Claude Code, and add a policy for it to your Access application. Then you store the token where Claude Code reads it, check that it works, and point Claude at the API reference.
6
6
 
@@ -77,9 +77,9 @@ Read GET /api/reference first, and use it for every call.
77
77
 
78
78
  The reference is also served as Markdown, `GET /api/reference.md`, which reads more easily for Claude than the JSON. So are the docs: `GET /api/docs/publishing.md` lists the publisher's pages, `GET /api/docs/self-hosting.md` this guide's, and each page is at `GET /api/docs/<slug>.md`.
79
79
 
80
- Claude reads the reference, and from there can list your posts, write a draft, and schedule it. The reference lists the body types each route takes. Most take JSON, sent with `Content-Type: application/json`. An image upload takes a multipart form, or the image file itself.
80
+ Claude reads the reference, and from there can list the posts, write a draft, and schedule it. The reference lists the body types each route takes. Most take JSON, sent with `Content-Type: application/json`. An image upload takes a multipart form, or the image file itself.
81
81
 
82
- To bring your subscribers over from another service, ask Claude to import your list. Your instance's `/migrate-list` skill reads the format from `GET /api/imports/format` and converts the old service's export to it. It checks Kestrel's plan against its own counts, and asks for your attestation before it imports anyone as a subscriber. [Bring your existing list](../publishing/01-your-list.md) explains what an import does, and the [import format](../publishing/08-import-format.md) describes every column.
82
+ To bring subscribers over from another service, ask Claude to import the list, as [Bring your existing list](../publishing/01-your-list.md) describes. Its [file format](../publishing/01-your-list.md#the-file-format) section describes every column.
83
83
 
84
84
  ## Check it
85
85
 
@@ -89,7 +89,7 @@ To bring your subscribers over from another service, ask Claude to import your l
89
89
  {"principal":{"kind":"service"},"auth":{"mode":"access"}}
90
90
  ```
91
91
 
92
- 1. Ask Claude to list your posts. It answers with the posts the editor shows.
92
+ 1. Ask Claude to list the posts. It answers with the posts the editor shows.
93
93
 
94
94
  1. Open a draft in the editor, and ask Claude to change a word in it. The editor shows that the draft was changed elsewhere, last edited by **Claude**.
95
95
 
@@ -1,14 +1,14 @@
1
1
  # Notifications through Cloudflare's email
2
2
 
3
- Kestrel emails you when a send goes out, and right away when one runs into a problem. By default, these notifications go through your email provider. The one you need most, about your provider refusing your account, is the one your provider would refuse too. Cloudflare's email doesn't depend on your provider, so that notification still reaches you.
3
+ Kestrel emails the publisher when a send finishes, and right away when one runs into a problem. By default, these notifications go through the email provider. The one that matters most, about the provider refusing your account, is the one the provider would refuse too. Cloudflare's email doesn't depend on the provider, so that notification still reaches the publisher.
4
4
 
5
- In this guide, you turn on Cloudflare's email for your app's hostname, and verify your own address with it. Then you add the binding that sends notifications through it, deploy, and send yourself a test.
5
+ In this guide, you turn on Cloudflare's email for your app's hostname, and verify the notifications address with it. Then you add the binding that sends notifications through it, deploy, and send yourself a test.
6
6
 
7
7
  ## Before you begin
8
8
 
9
9
  You need:
10
10
 
11
- - Your notification address saved, from [Verify it works](../get-started/05-verify.md#3-set-up-your-notifications).
11
+ - The notifications address saved, from [Verify it works](../get-started/05-verify.md#3-set-up-notifications).
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
@@ -23,13 +23,13 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
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/04-ses.md#3-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
25
25
 
26
- ## 2. Verify your address
26
+ ## 2. Verify the notifications address
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.
29
29
 
30
30
  1. Under **Email Routing**, go to **Destination Addresses**, and add the address you saved for notifications.
31
31
 
32
- 1. Open the email Cloudflare sends to that address, and select **Verify email address**.
32
+ 1. Open the email Cloudflare sends to that address, and select **Verify email address**. If it's the publisher's address, ask them to do this step.
33
33
 
34
34
  A verified address belongs to your Cloudflare account, so every environment in it can use it.
35
35
 
@@ -41,11 +41,11 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
41
41
 
42
42
  ```jsonc
43
43
  "send_email": [
44
- { "name": "NOTIFY", "allowed_destination_addresses": ["you@example.com"] } // ← your address
44
+ { "name": "NOTIFY", "allowed_destination_addresses": ["you@example.com"] } // ← the notifications address
45
45
  ],
46
46
  ```
47
47
 
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.
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 changes the notifications address later, verify the new one and add it here too.
49
49
 
50
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`:
51
51
 
@@ -76,13 +76,13 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
76
76
 
77
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`.
78
78
 
79
- 1. Select **Send a test notification**. It arrives at your address, and **Last notification** shows it as delivered.
79
+ 1. Select **Send a test notification**. It arrives at the notifications address, and **Last notification** shows it as delivered.
80
80
 
81
- If the test isn't delivered, **Last notification** shows Cloudflare's reason, such as an address that isn't verified. Cloudflare's Email Routing summary lists notifications as dropped even when they're delivered, so check your inbox rather than the summary.
81
+ If the test isn't delivered, **Last notification** shows Cloudflare's reason, such as an address that isn't verified. Cloudflare's Email Routing summary lists notifications as dropped even when they're delivered, so check the notifications inbox rather than the summary.
82
82
 
83
83
  ## What notifications don't cover
84
84
 
85
- - **The schedule stopping.** The same once-a-minute schedule that sends your 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/05-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
 
@@ -0,0 +1,62 @@
1
+ # As your list grows
2
+
3
+ A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits. That suits a list of up to about 5,000 subscribers. Past that, two limits matter: how fast SES lets you send, and how many database writes the free plan allows a day.
4
+
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
+
7
+ ## 1. Raise your SES limits
8
+
9
+ SES caps how many emails your account sends a day, and how many a second. A send that hits the daily quota waits, emails you, and resumes on its own once the quota frees up. Raising your maximum send rate to 50 a second costs nothing, and it makes every send faster.
10
+
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
+
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`](../reference/01-configuration.md#ses_max_send_rate) to match.
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
+ ```
27
+
28
+ ## 2. Upgrade to Workers Paid
29
+
30
+ On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) allows a fixed number of database writes a day. A send to about 7,000 recipients uses them all, and then the whole app stops until midnight UTC. Until you upgrade, the app warns the publisher before a send or an import that could use up the day. It's recommended to upgrade from about 5,000 subscribers.
31
+
32
+ 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.
33
+
34
+ 1. In `wrangler.jsonc`, add `"SUBREQUEST_BUDGET": "10000"` to the `production` block's `vars`. It tells the app it's on Workers Paid, so each minute sends far more ([`SUBREQUEST_BUDGET`](../reference/01-configuration.md#subrequest_budget)).
35
+
36
+ 1. Commit the change, and push it:
37
+
38
+ ```bash
39
+ git commit -am "Run on Workers Paid"
40
+ git push
41
+ ```
42
+
43
+ 1. Deploy:
44
+
45
+ ```bash
46
+ npx wrangler deploy --env production
47
+ ```
48
+
49
+ ## How long a send takes
50
+
51
+ | Setup | 1,000 subscribers | 5,000 subscribers | 10,000 subscribers |
52
+ | --- | --- | --- | --- |
53
+ | Free plan, a starting SES rate of 14 | about 18 minutes | about 1.5 hours | over the free plan's daily writes |
54
+ | Free plan, SES rate raised to 50 | about 5 minutes | about 25 minutes | over the free plan's daily writes |
55
+ | Workers Paid, SES rate of 14 | about 2 minutes | about 7 minutes | about 15 minutes |
56
+ | Workers Paid, SES rate of 50 | under a minute | about 2 minutes | about 4 minutes |
57
+
58
+ ## Check it
59
+
60
+ 1. The SES **Account dashboard** shows the maximum send rate you set in `SES_MAX_SEND_RATE`.
61
+
62
+ 1. On Workers Paid, the import screen under **Subscribers → Import** reads "CSV or .txt, up to 10,000 rows."