@kurtbruns/kestrel 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dev.vars.example +10 -0
- package/CHANGELOG.md +30 -1
- package/bin/kestrel.mjs +1 -1
- package/cli/dev.mjs +3 -3
- package/cli/init.mjs +98 -4
- package/dist/public/dashboard/app.js +288 -286
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +6 -3
- package/dist/public/dashboard/styles.css +10 -0
- package/dist/worker/index.js +5606 -5095
- package/docs/README.md +5 -3
- package/docs/get-started/02-deploy.md +24 -20
- package/docs/get-started/03-access.md +9 -5
- package/docs/get-started/04-aws-account.md +68 -0
- package/docs/get-started/{04-ses.md → 05-ses.md} +89 -92
- package/docs/get-started/06-production-access.md +64 -0
- package/docs/get-started/{05-verify.md → 07-verify.md} +16 -26
- package/docs/get-started/{06-go-live.md → 08-go-live.md} +2 -2
- package/docs/guides/01-allow-claude.md +1 -1
- package/docs/guides/02-automate.md +1 -1
- package/docs/guides/03-notifications.md +3 -3
- package/docs/guides/07-upgrade.md +4 -4
- package/docs/guides/09-as-your-list-grows.md +8 -18
- package/docs/publishing/02-sending-rules.md +2 -2
- package/docs/publishing/03-name-your-publication.md +2 -0
- package/docs/publishing/04-write-a-post.md +1 -1
- package/docs/publishing/06-troubleshooting.md +11 -7
- package/docs/publishing/09-share-your-subscribe-link.md +1 -1
- package/docs/publishing/10-check-your-setup.md +4 -2
- package/docs/reference/01-configuration.md +14 -12
- package/docs/reference/02-sending-domain-dns.md +4 -4
- package/migrations/0015_provider_account.sql +18 -0
- package/package.json +1 -1
- package/template/claude/CLAUDE.md +3 -3
- package/template/wrangler.jsonc +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Request production access
|
|
2
|
+
|
|
3
|
+
Once Amazon SES is connected, request production access to start sending email to subscribers. AWS reviews each request by hand, and its first answer can take up to 96 hours. Its reviewer looks for what's now in place: a website that says what the newsletter is, a subscribe form that confirms each signup by email, and bounces and complaints that reach the app.
|
|
4
|
+
|
|
5
|
+
In this guide, you name the publication, submit the request, and answer any questions AWS asks. Then, once AWS answers, you check that it granted access.
|
|
6
|
+
|
|
7
|
+
## Before you begin
|
|
8
|
+
|
|
9
|
+
- Amazon SES connected, from [Connect Amazon SES](05-ses.md).
|
|
10
|
+
- The publication's name and tagline, from the publisher.
|
|
11
|
+
|
|
12
|
+
## 1. Name the publication
|
|
13
|
+
|
|
14
|
+
AWS may visit the publication's website to review the request. On a new instance with no posts, the tagline is the only line on the home page that says what the newsletter is about.
|
|
15
|
+
|
|
16
|
+
1. In the editor, open **Settings → Publication identity**, and enter the name and tagline. Both can change later.
|
|
17
|
+
|
|
18
|
+
1. If the newsletter will mainly sell something, add a mailing address there too, with **Add a mailing address**. [When to provide a mailing address](../publishing/03-name-your-publication.md#when-to-provide-a-mailing-address) explains when US law asks for one.
|
|
19
|
+
|
|
20
|
+
1. Open `https://newsletter.example.com/` in a browser. The home page shows the tagline, and a subscribe form.
|
|
21
|
+
|
|
22
|
+
## 2. Submit the request
|
|
23
|
+
|
|
24
|
+
AWS reviews each request by hand, for the region the app sends from.
|
|
25
|
+
|
|
26
|
+
1. In the [SES console](https://console.aws.amazon.com/ses/), choose the region the app sends from, the one in `AWS_REGION`. Open the **Account dashboard**, select **View Get set up page**, and then **Request production access**.
|
|
27
|
+
|
|
28
|
+
1. Fill in the request:
|
|
29
|
+
|
|
30
|
+
- **Mail type:** Marketing
|
|
31
|
+
- **Website URL:** `https://newsletter.example.com/`
|
|
32
|
+
|
|
33
|
+
Leave the other fields as they are.
|
|
34
|
+
|
|
35
|
+
1. Check the acknowledgement, and select **Submit request**. AWS Support answers by email.
|
|
36
|
+
|
|
37
|
+
1. Within about ten minutes, the editor's **Settings → Email sender** says AWS is reviewing your request for production access. It also says when AWS grants or denies it.
|
|
38
|
+
|
|
39
|
+
## 3. Answer AWS's questions
|
|
40
|
+
|
|
41
|
+
AWS Support may ask how you collect addresses and handle bounces before it decides. A newsletter may be asked for more detail, or denied the first time, sometimes without a reason. A reply that explains how the list is built usually gets further than a new request. If it asks for more detail, or denies the request, reply in the support case, or open a new request from the **Account dashboard**.
|
|
42
|
+
|
|
43
|
+
In the sandbox, the subscribe form's confirmation email reaches only addresses verified in SES. If AWS's reviewer tries the form, say that this is why no confirmation arrives. In your reply, cover:
|
|
44
|
+
|
|
45
|
+
- That signups use double opt-in: each subscriber confirms by email before joining the list.
|
|
46
|
+
- That every post offers one-click unsubscribe.
|
|
47
|
+
- That bounces and complaints reach the app, and it stops mailing those addresses automatically. [Verify it works](07-verify.md#6-check-that-bounces-and-complaints-come-back) shows it working.
|
|
48
|
+
- That the publication's website shows what the newsletter is, and how readers subscribe.
|
|
49
|
+
- That an imported list was gathered with opt-in on the previous service.
|
|
50
|
+
- How many subscribers the newsletter expects, and how often it sends.
|
|
51
|
+
|
|
52
|
+
## 4. Check that AWS granted it
|
|
53
|
+
|
|
54
|
+
Until AWS grants production access, SES sends only to your verified addresses, so check it before you go live.
|
|
55
|
+
|
|
56
|
+
1. In the editor, open **Settings → Email sender**. The email provider reads **Production**. From the API, `npx kestrel api GET /api/provider` shows `state` as `production`.
|
|
57
|
+
|
|
58
|
+
1. If it reads **Sandbox**, AWS hasn't granted access yet. Meanwhile, continue with [Verify it works](07-verify.md), which runs in the sandbox. Come back to this section when AWS answers, and check the email AWS Support sends for questions.
|
|
59
|
+
|
|
60
|
+
1. If it says Kestrel's AWS key needs permission to read your account, select **Add the permission** and follow the step it opens. The app checks again within an hour.
|
|
61
|
+
|
|
62
|
+
1. If it reads just "Amazon SES", with nothing under it, the app hasn't heard from SES yet. Look again in ten minutes. If it still reads that way, the app's logs say why, in their `provider.account_checked` line, as [Read the logs](07-verify.md#7-read-the-logs) describes.
|
|
63
|
+
|
|
64
|
+
Next, [verify it works](07-verify.md).
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
This page checks your instance end to end with real email, while the list is empty and every email goes only to you. You confirm that inboxes trust your mail, that posts look right in a mail client, and that bounces, complaints, and unsubscribes reach the app.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You can do all of this while SES is still in the sandbox, since every email goes to the addresses you verified in [Connect Amazon SES](05-ses.md#3-verify-your-test-addresses) or to SES's test addresses.
|
|
6
|
+
|
|
7
|
+
In this guide, you send yourself a test email, reply to it, and check that it passes authentication. You check that notifications arrive, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, read the app's logs, and clean up the test subscribers.
|
|
6
8
|
|
|
7
9
|
## Before you begin
|
|
8
10
|
|
|
9
|
-
- The two addresses you verified in SES in [Connect Amazon SES](
|
|
11
|
+
- The two addresses you verified in SES in [Connect Amazon SES](05-ses.md#3-verify-your-test-addresses). Some steps use both.
|
|
10
12
|
- A Gmail address, or another mail client that shows a message's original source and an **Unsubscribe** button.
|
|
11
13
|
|
|
12
14
|
## 1. Send yourself a test email
|
|
@@ -19,7 +21,7 @@ A test email goes through the same code as a real send. If the test looks right,
|
|
|
19
21
|
|
|
20
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.
|
|
21
23
|
|
|
22
|
-
1. Send a test email to your second address, not the publisher's inbox, and reply to it from there, as a reader would. The reply arrives in the publisher's inbox, the one you chose in [Connect Amazon SES](
|
|
24
|
+
1. Send a test email to your second address, not the publisher's inbox, and reply to it from there, as a reader would. The reply arrives in the publisher's inbox, the one you chose in [Connect Amazon SES](05-ses.md#8-receive-replies-at-your-from-address), or in your From address's own mailbox if you skipped forwarding. If it bounces, check the routing rule and the destination address there.
|
|
23
25
|
|
|
24
26
|
## 2. Check that it passes authentication
|
|
25
27
|
|
|
@@ -33,21 +35,19 @@ Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing
|
|
|
33
35
|
- `dmarc=pass` for `send.example.com`
|
|
34
36
|
- `spf=pass`, for a hostname under `send.example.com`
|
|
35
37
|
|
|
36
|
-
If SPF passes for `amazonses.com` instead, SES couldn't read your custom MAIL FROM domain's MX record, and used its own. DMARC still passes, but check the records from [Connect Amazon SES](
|
|
37
|
-
|
|
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.
|
|
38
|
+
If SPF passes for `amazonses.com` instead, SES couldn't read your custom MAIL FROM domain's MX record, and used its own. DMARC still passes, but check the MAIL FROM records from [Connect Amazon SES](05-ses.md#1-verify-your-sending-hostname).
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
If a check fails, compare your DNS records with the ones in [Connect Amazon SES](05-ses.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
## 3. Check that notifications arrive
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Kestrel emails a notifications address when a send finishes, and right away when one runs into a problem. Check that they reach you, using one of the addresses you verified in SES.
|
|
45
45
|
|
|
46
|
-
1.
|
|
46
|
+
1. In the editor, open **Settings → Notifications**, enter that address, and select **Save**.
|
|
47
47
|
|
|
48
48
|
1. Select **Send a test notification**. It arrives at that address.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
The publisher sets their own address in [Check your setup](../publishing/10-check-your-setup.md).
|
|
51
51
|
|
|
52
52
|
## 4. Send a real post to yourself
|
|
53
53
|
|
|
@@ -86,13 +86,13 @@ When an address bounces or marks your mail as spam, SES tells the app through SN
|
|
|
86
86
|
|
|
87
87
|
1. In the editor, **Subscribers** lists the bounce address as **Pending** with a **bounced** flag, and the complaint address as **Unsubscribed**. Open its row: its page says it left by a complaint. The app mails neither again.
|
|
88
88
|
|
|
89
|
-
If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set in [Send bounces and complaints to the app](
|
|
89
|
+
If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set in [Send bounces and complaints to the app](05-ses.md#4-send-bounces-and-complaints-to-the-app), and the subscription in [Subscribe the app to the topic](05-ses.md#7-subscribe-the-app-to-the-topic).
|
|
90
90
|
|
|
91
91
|
## 7. Read the logs
|
|
92
92
|
|
|
93
93
|
The app writes a log line for each thing it does, and Cloudflare keeps them for a few days, depending on your plan. Nothing else tells you if the once-a-minute schedule that sends posts stops, so it's worth knowing where to look.
|
|
94
94
|
|
|
95
|
-
1. In the Cloudflare dashboard, go to **Workers & Pages**, select `
|
|
95
|
+
1. In the Cloudflare dashboard, go to **Workers & Pages**, select your Worker, the `name` in the `production` block of `wrangler.jsonc`, and select **Observability**.
|
|
96
96
|
|
|
97
97
|
1. Filter on `event` equal to `sweep.tick`. A line arrives every minute.
|
|
98
98
|
|
|
@@ -104,17 +104,7 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
|
|
|
104
104
|
|
|
105
105
|
To follow one send, narrow the time range and filter on its `sendId`, since a long range may show only a sample. On the Free plan, Cloudflare stops keeping logs for the rest of the day once its daily allowance is used, as [its pricing page](https://developers.cloudflare.com/observability/pricing/) describes.
|
|
106
106
|
|
|
107
|
-
## 8.
|
|
108
|
-
|
|
109
|
-
[Connect Amazon SES](04-ses.md#4-request-production-access) asked AWS for production access. Until AWS grants it, the sandbox mails only your verified addresses, so check it before you go live.
|
|
110
|
-
|
|
111
|
-
1. In SES, open the **Account dashboard** in the region the app sends from. It no longer says your account is in the sandbox, and it shows your daily quota and maximum send rate.
|
|
112
|
-
|
|
113
|
-
1. Check that `SES_MAX_SEND_RATE` in `wrangler.jsonc` matches that rate, and that you deployed after setting it, as [Connect Amazon SES](04-ses.md#7-set-the-provider-and-deploy) describes.
|
|
114
|
-
|
|
115
|
-
If the dashboard still says sandbox, AWS hasn't answered yet. Every other section works in the sandbox, so come back to this one when it does.
|
|
116
|
-
|
|
117
|
-
## 9. Clean up the test subscribers
|
|
107
|
+
## 8. Clean up the test subscribers
|
|
118
108
|
|
|
119
109
|
The checks leave test addresses on the list. Erase them, so the publisher starts with only real readers.
|
|
120
110
|
|
|
@@ -126,6 +116,6 @@ The checks leave test addresses on the list. Erase them, so the publisher starts
|
|
|
126
116
|
|
|
127
117
|
Erasing keeps only the address, so it's never mailed again, but the person can subscribe again through the subscribe page. Erasing your own address doesn't lock you out.
|
|
128
118
|
|
|
129
|
-
When every section gives the result it describes, your instance is ready
|
|
119
|
+
When every section gives the result it describes and AWS grants production access, your instance is ready to go live.
|
|
130
120
|
|
|
131
|
-
Next, [go live](
|
|
121
|
+
Next, [go live](08-go-live.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Go live
|
|
2
2
|
|
|
3
|
-
At this point, your instance should be deployed
|
|
3
|
+
At this point, your instance should be deployed and verified. Before anyone subscribes, check that AWS granted production access, as [Check that AWS granted it](06-production-access.md#4-check-that-aws-granted-it) describes, so SES can send to anyone. From here, the publisher can set up the publication, and you can continue with the guides below.
|
|
4
4
|
|
|
5
5
|
## Start publishing
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ The rest happens in the editor, at `https://newsletter.example.com/dashboard/`.
|
|
|
8
8
|
|
|
9
9
|
If someone else is the publisher, let them sign in first:
|
|
10
10
|
|
|
11
|
-
1. Add their email address to the
|
|
11
|
+
1. Add their email address to the policy on the Access application that lets you in, as in [Allow yourself in](03-access.md#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, store the list again with their address added.
|
|
12
12
|
|
|
13
13
|
1. If they don't have a Cloudflare account, add another way to sign in, such as a one-time code by email. It's under **Zero Trust → Integrations → Identity providers**.
|
|
14
14
|
|
|
@@ -57,7 +57,7 @@ If you're the publisher, connect now, as [Connect Claude](../publishing/08-conne
|
|
|
57
57
|
|
|
58
58
|
## Who can connect
|
|
59
59
|
|
|
60
|
-
Anyone the
|
|
60
|
+
Anyone the Access application's Allow policy lets into the dashboard can connect Claude, since they sign in the same way. To stop someone connecting, remove them from that policy. If you set `ACCESS_ALLOWED_EMAILS`, store the list again without them: the app checks it on every request, so it takes effect at once.
|
|
61
61
|
|
|
62
62
|
## Check it
|
|
63
63
|
|
|
@@ -25,7 +25,7 @@ A service token is a Client ID and a Client Secret that a program sends with eac
|
|
|
25
25
|
|
|
26
26
|
## 2. Allow the token in
|
|
27
27
|
|
|
28
|
-
The application lets in only the people its
|
|
28
|
+
The application lets in only the people its Allow policy names, so the token needs a policy of its own on the same application.
|
|
29
29
|
|
|
30
30
|
1. Under **Access controls → Applications**, select **Configure** on the application from [Lock the dashboard with Access](../get-started/03-access.md#2-create-the-access-application).
|
|
31
31
|
|
|
@@ -8,7 +8,7 @@ In this guide, you turn on Cloudflare's email for your app's hostname, and verif
|
|
|
8
8
|
|
|
9
9
|
You need:
|
|
10
10
|
|
|
11
|
-
- The notifications address saved, from [Verify it works](../get-started/
|
|
11
|
+
- The notifications address saved, from [Verify it works](../get-started/07-verify.md#3-check-that-notifications-arrive).
|
|
12
12
|
- Your domain's DNS on Cloudflare, as for the rest of the app.
|
|
13
13
|
|
|
14
14
|
## 1. Turn on Email Routing for your app's hostname
|
|
@@ -21,7 +21,7 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
|
|
|
21
21
|
|
|
22
22
|
1. Cloudflare adds MX, SPF, and DKIM records for `newsletter.example.com`, and locks them. Wait until they show as added.
|
|
23
23
|
|
|
24
|
-
Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/
|
|
24
|
+
Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/05-ses.md#8-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
|
|
25
25
|
|
|
26
26
|
## 2. Verify the notifications address
|
|
27
27
|
|
|
@@ -82,7 +82,7 @@ If the test isn't delivered, **Last notification** shows Cloudflare's reason, su
|
|
|
82
82
|
|
|
83
83
|
## What notifications don't cover
|
|
84
84
|
|
|
85
|
-
- **The schedule stopping.** The same once-a-minute schedule that sends posts also notices problems. If the schedule itself stops, nothing tells you. [Read the logs](../get-started/
|
|
85
|
+
- **The schedule stopping.** The same once-a-minute schedule that sends posts also notices problems. If the schedule itself stops, nothing tells you. [Read the logs](../get-started/07-verify.md#7-read-the-logs) to check that `sweep.tick` still arrives every minute.
|
|
86
86
|
- **A notification that can't get through.** The app tries each one up to five times, a minute apart. A failure shows under **Settings → Notifications**, and in the logs as `notify.failed`.
|
|
87
87
|
- **Problems while no address is set.** With the address blank, the app sends nothing, and saves nothing up for later.
|
|
88
88
|
|
|
@@ -78,7 +78,7 @@ D1 keeps a history of your database, called Time Travel, for 30 days on Workers
|
|
|
78
78
|
1. Note where the database stands now:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
npx wrangler d1 time-travel info
|
|
81
|
+
npx wrangler d1 time-travel info DB --env production
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
It prints a bookmark for the database's current state. Copy it, and keep it until the upgrade checks out.
|
|
@@ -117,9 +117,9 @@ The output ends with your app's hostname.
|
|
|
117
117
|
|
|
118
118
|
1. Reload the editor. The foot of the **Docs** tab's contents list shows the new version, which links to its release page.
|
|
119
119
|
|
|
120
|
-
1. Send yourself a test email, as in [Verify it works](../get-started/
|
|
120
|
+
1. Send yourself a test email, as in [Verify it works](../get-started/07-verify.md#1-send-yourself-a-test-email). Don't send a real post to check an upgrade: on a live instance, it goes to your whole list.
|
|
121
121
|
|
|
122
|
-
1. In the logs, `sweep.tick` still arrives every minute, as in [Verify it works](../get-started/
|
|
122
|
+
1. In the logs, `sweep.tick` still arrives every minute, as in [Verify it works](../get-started/07-verify.md#7-read-the-logs).
|
|
123
123
|
|
|
124
124
|
## Go back
|
|
125
125
|
|
|
@@ -147,7 +147,7 @@ If the new release misbehaves, deploy the previous one, and put the database bac
|
|
|
147
147
|
1. Restore the database to the bookmark from section 3:
|
|
148
148
|
|
|
149
149
|
```bash
|
|
150
|
-
npx wrangler d1 time-travel restore
|
|
150
|
+
npx wrangler d1 time-travel restore DB --env production --bookmark=REPLACE_WITH_BOOKMARK
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
1. Push the revert to your repository:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# As your list grows
|
|
2
2
|
|
|
3
|
-
A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits. Workers Free suits a list of up to about 1,000 subscribers. Past about 5,000, one send can use up the day's database writes and stop the newsletter until midnight UTC.
|
|
3
|
+
A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits. Workers Free suits a list of up to about 1,000 subscribers. Past about 5,000, one send can use up the day's database writes and stop the newsletter until midnight UTC. As the list grows, two limits start to matter: how fast a send goes out, and how many database writes the free plan allows a day.
|
|
4
4
|
|
|
5
5
|
In this guide, you raise your SES limits, which helps on either plan, and upgrade to Workers Paid, which a larger list needs. Then you see how long a send takes after each.
|
|
6
6
|
|
|
@@ -10,26 +10,16 @@ SES caps how many emails your account sends a day, and how many a second. A send
|
|
|
10
10
|
|
|
11
11
|
1. Ask AWS for a higher daily quota and a maximum send rate of 50, as AWS's [Increasing your Amazon SES sending quotas](https://docs.aws.amazon.com/ses/latest/dg/manage-sending-quotas-request-increase.html) describes.
|
|
12
12
|
|
|
13
|
-
1. When AWS grants it, the
|
|
14
|
-
|
|
15
|
-
1. Commit the change, and push it:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
git commit -am "Match the SES send rate"
|
|
19
|
-
git push
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
1. Deploy:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npx wrangler deploy --env production
|
|
26
|
-
```
|
|
13
|
+
1. When AWS grants it, the app takes the new rate from your account within about ten minutes, and **Settings → Email sender** shows it under **Send rate**. If `SES_MAX_SEND_RATE` is set in `wrangler.jsonc`, the app keeps to the lower of the two, so remove it or raise it to match, then deploy, as [Configuration](../reference/01-configuration.md#vars) describes.
|
|
27
14
|
|
|
28
15
|
## 2. Upgrade to Workers Paid
|
|
29
16
|
|
|
30
|
-
|
|
17
|
+
Cloudflare's $5 Workers Paid plan helps in two ways, at any size of list.
|
|
18
|
+
|
|
19
|
+
- **Faster sends.** On the free plan, Cloudflare caps how much one minute's work can do. A send then uses your SES rate for only a few seconds of each minute: about 56 emails a minute at a rate of 14. On Workers Paid, a send uses about 50 seconds of each minute, about 700 emails a minute at the same rate. If sends feel slow, upgrading speeds them up more than raising your SES rate does. [How long a send takes](#how-long-a-send-takes) compares them.
|
|
20
|
+
- **No daily limit on database writes.** On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) allows 100,000 written rows a day, reset at midnight UTC. A send writes about 14 rows a recipient, receipts included, and an import about 7 for each new subscriber. So at 1,000 subscribers, an import of the whole list and two sends on one day use about a third of the day. Once the day's rows are spent, the whole app stops until midnight UTC, the SES webhook with it. That day's bounces and complaints are lost, and suppress or unsubscribe no one.
|
|
31
21
|
|
|
32
|
-
It's recommended to upgrade from about 1,000 subscribers. Past that, the Dashboard and the Subscribers screen say so, and so does an import that would take the list past it. Past about 5,000, a send that could use up the day on its own also warns before it's scheduled. The app doesn't count the day's writes, so between the two, two large sends or several imports and a send on one UTC day can still use it up without a warning at that moment.
|
|
22
|
+
It's recommended to upgrade from about 1,000 subscribers. Past that, the Dashboard and the Subscribers screen say so, and so does an import that would take the list past it. Past about 5,000, a send that could use up the day on its own also warns before it's scheduled. The app doesn't count the day's writes, so between the two, two large sends or several imports and a send on one UTC day can still use it up without a warning at that moment.
|
|
33
23
|
|
|
34
24
|
1. Upgrade your Cloudflare account to the $5 [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/), which has no daily limit on writes.
|
|
35
25
|
|
|
@@ -61,6 +51,6 @@ Over years, one more limit can matter: on the free plan, one [D1 database](https
|
|
|
61
51
|
|
|
62
52
|
## Check it
|
|
63
53
|
|
|
64
|
-
1.
|
|
54
|
+
1. **Settings → Email sender** shows your new maximum send rate under **Send rate**, "from your SES account".
|
|
65
55
|
|
|
66
56
|
1. On Workers Paid, the import screen under **Subscribers → Import** reads "CSV or .txt, up to 10,000 rows."
|
|
@@ -9,14 +9,14 @@ This page covers what Kestrel handles, what you handle, and how to act on a read
|
|
|
9
9
|
- **Consent.** Kestrel mails only people who confirmed they want your newsletter. Someone who subscribes gets an email with a link, and only once they confirm do they receive posts. People you import come with your old service's record of their consent, and your word for it ([Bring your existing list](01-your-list.md#what-carries-over)).
|
|
10
10
|
- **Unsubscribing.** Every email has an unsubscribe link, and the one-click unsubscribe header that Gmail and Yahoo ask bulk senders for. It takes effect at once. US law allows up to 10 business days.
|
|
11
11
|
- **Bounces and complaints.** An address that bounces is never mailed again, so a bad address can't keep hurting your sending reputation. A reader who marks your email as spam is unsubscribed, as if they had used the link, and you can't add them back: only their own sign-up does.
|
|
12
|
-
- **Sender authentication.** Your emails are signed with DKIM for your own domain, and SPF and DMARC records in its DNS vouch for them. You set them up in [Connect Amazon SES](../get-started/
|
|
12
|
+
- **Sender authentication.** Your emails are signed with DKIM for your own domain, and SPF and DMARC records in its DNS vouch for them. You set them up in [Connect Amazon SES](../get-started/05-ses.md), and mailbox providers now expect them.
|
|
13
13
|
|
|
14
14
|
## What you handle
|
|
15
15
|
|
|
16
16
|
- **An honest From name and subject line.** The From name should say who the email is from, and the subject should match what's inside. The From name and address are set when Kestrel is deployed ([Deploy the app](../get-started/02-deploy.md#4-configure-production)).
|
|
17
17
|
- **Marking ads as ads.** When an email is an advertisement, say so clearly. In a newsletter that's mostly writing, labeling a sponsor's section is enough.
|
|
18
18
|
- **A mailing address, when an email is commercial.** Most newsletters don't need one. [When to provide a mailing address](03-name-your-publication.md#when-to-provide-a-mailing-address) says when you do, beside where you add one.
|
|
19
|
-
- **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, as [Connect Amazon SES](../get-started/
|
|
19
|
+
- **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, as [Connect Amazon SES](../get-started/05-ses.md#8-receive-replies-at-your-from-address) sets up. Kestrel never sees a reply, so carrying out what a reader asks is up to you.
|
|
20
20
|
|
|
21
21
|
## When a reader asks to leave or to be deleted
|
|
22
22
|
|
|
@@ -4,6 +4,8 @@ Give your publication a name and a look before your first post. Set the name, ta
|
|
|
4
4
|
|
|
5
5
|
All three are optional, and you can change them at any time. How they appear in each email comes from your email template, which you can [edit](11-email-template.md).
|
|
6
6
|
|
|
7
|
+
Write the tagline as one line that says what the newsletter covers and how often it goes out, such as "Monthly notes on the birds of the Hudson Valley." Until your first post, it's the only line on your home page that says what readers are signing up for. It's also what AWS sees if it reviews your site when you [request production access](../get-started/06-production-access.md).
|
|
8
|
+
|
|
7
9
|
## When to provide a mailing address
|
|
8
10
|
|
|
9
11
|
**If your newsletter is mostly your own writing, you probably don't need one. If an email is mainly selling something, add one.**
|
|
@@ -16,7 +16,7 @@ The editor takes Markdown, and Kestrel saves as you type. The same Markdown beco
|
|
|
16
16
|
|
|
17
17
|
Choose **Schedule**, pick a time under **Send at**, and confirm. The time must be at least five minutes away by default, or the lead whoever runs your instance set. That's your window to catch a mistake, and you can cancel or move the send until then. **Send now** sends after the same wait.
|
|
18
18
|
|
|
19
|
-
**Then send yourself a test.** Scheduling locks the post and makes its email, so **Send test email** now sends exactly what your list receives. **Settings → Default test recipients** fills in the addresses. A test needs an email provider, which [Connect Amazon SES](../get-started/
|
|
19
|
+
**Then send yourself a test.** Scheduling locks the post and makes its email, so **Send test email** now sends exactly what your list receives. **Settings → Default test recipients** fills in the addresses. A test needs an email provider, which [Connect Amazon SES](../get-started/05-ses.md) sets up.
|
|
20
20
|
|
|
21
21
|
To change the post, choose **Cancel** on its banner, and it's a draft again. **Edit schedule** moves the time without unlocking it.
|
|
22
22
|
|
|
@@ -8,10 +8,10 @@ When something needs you, Kestrel says so in a line on the send's page, under **
|
|
|
8
8
|
|
|
9
9
|
Nothing is lost. A problem with your email provider never marks anyone unsent or mails anyone twice. The send waits where it is, and carries on once the problem is fixed. The send's line names the fix. When that fix is outside Kestrel, whoever runs your instance makes it, and it's one of these:
|
|
10
10
|
|
|
11
|
-
- **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/
|
|
12
|
-
- **SES keeps refusing for its sending rate.** Kestrel is
|
|
13
|
-
- **No email provider is connected.** Connect one, as [Connect Amazon SES](../get-started/
|
|
14
|
-
- **A send missed its time, or nothing has gone out for a while.** The part of Kestrel that sends has stopped, or keeps failing on this send. Check the Worker's Cron Trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes, and read the app's logs, as [Read the logs](../get-started/
|
|
11
|
+
- **Amazon SES refuses your account or the send.** The line carries SES's words and the fix, almost always in the SES console. [Connect Amazon SES](../get-started/05-ses.md) sets up each piece SES checks: the sending domain, the AWS key, and the configuration set. [Request production access](../get-started/06-production-access.md) lifts the sandbox. When SES refuses your account, any other post that comes due meanwhile waits with it. When it refuses only this send's email, other posts go out as usual. While SES takes some readers and refuses the rest, as while a daily quota frees up, those it takes are sent. The line stays until SES takes everyone.
|
|
12
|
+
- **SES keeps refusing for its sending rate.** Kestrel is sending faster than your SES account allows. **Settings → Email sender** shows the **Send rate** and where it comes from. If `SES_MAX_SEND_RATE` sets it, lower it to the maximum send rate on the SES console's account dashboard, or remove it, then deploy, as [Configuration](../reference/01-configuration.md#vars) describes. If it's Kestrel's default, the app can't read your account: allow its key `ses:GetAccount`, as [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) shows.
|
|
13
|
+
- **No email provider is connected.** Connect one, as [Connect Amazon SES](../get-started/05-ses.md) describes, and the send goes out at its next retry.
|
|
14
|
+
- **A send missed its time, or nothing has gone out for a while.** The part of Kestrel that sends has stopped, or keeps failing on this send. Check the Worker's Cron Trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes, and read the app's logs, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes. If another send is going out at the same time, this one may only be waiting its turn.
|
|
15
15
|
|
|
16
16
|
## Someone was mailed twice, or not at all
|
|
17
17
|
|
|
@@ -27,11 +27,11 @@ Under **Subscribers**, search for their address and open their row. Their page l
|
|
|
27
27
|
|
|
28
28
|
That's normal. **Delivered** counts only the emails your provider confirmed reached the reader's mail server. Confirmations can lag by days, so the counts keep rising after a send finishes. Some are never confirmed at all, so a send can read **Settling** for good, as long as some receipts have come.
|
|
29
29
|
|
|
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/
|
|
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.
|
|
31
31
|
|
|
32
32
|
## A test doesn't arrive
|
|
33
33
|
|
|
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
|
|
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).
|
|
35
35
|
|
|
36
36
|
## Your list outgrows Workers Free
|
|
37
37
|
|
|
@@ -62,9 +62,13 @@ Most failures are in your instance's Cloudflare Access setup, made by whoever ru
|
|
|
62
62
|
- **Claude says it couldn't connect, or the sign-in failed.** Your instance's Access application needs Managed OAuth set up as [Turn on Managed OAuth](../guides/01-allow-claude.md#1-turn-on-managed-oauth) describes, including Claude's sign-in address for Claude Desktop and claude.ai. Check that the connector's address is your publication's address followed by `/api/mcp`, and that your instance is deployed, not running on your own computer.
|
|
63
63
|
- `kestrel login` says your instance "doesn't offer a sign-in for programs". Managed OAuth is off. Turn it on, as the same section describes.
|
|
64
64
|
- `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the address you signed in with. If either names `access_unidentified`, it's the next problem. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
|
|
65
|
-
- **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the
|
|
65
|
+
- **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the policy that lets you in by email. Whoever runs your instance removes the other policy from its Access application, or changes it to one of those two kinds. A script that gets this needs a service token under a Service Auth policy, as [Allow the token in](../guides/02-automate.md#2-allow-the-token-in) describes.
|
|
66
66
|
- **Claude's changes read "via the API".** Claude called the API some way other than `kestrel api`, which names Claude Code. Your instance's instructions for Claude Code have it use `kestrel api` once they're brought up to date, as [Install the release](../guides/07-upgrade.md#2-install-the-release) describes. Changes reading "a service token" mean Claude still uses one, and [Connect Claude](08-connect-claude.md#claude-code) signs it in as you instead.
|
|
67
67
|
|
|
68
|
+
## Cloudflare won't save the Access application
|
|
69
|
+
|
|
70
|
+
Whoever sets up your instance can meet this in [Lock the dashboard with Access](../get-started/03-access.md#2-create-the-access-application). Saving the application fails with an error naming `use_clientless_isolation_app_launcher_url`, saying it needs private destinations. The form has an empty private hostname or IP row beside the two public hostnames. Remove that row, and save again.
|
|
71
|
+
|
|
68
72
|
## Notifications
|
|
69
73
|
|
|
70
74
|
With an address set, Kestrel emails you:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Your publication's subscribe page is at `/subscribe` on your publication's address, such as `https://newsletter.example.com/subscribe`. Copy it from the dashboard's checklist or **Settings → Ways to subscribe**, and share it wherever your readers are. A reader joins only once they confirm by email.
|
|
4
4
|
|
|
5
|
-
**Signups open only once an email provider is connected.** Until then, the page says the newsletter isn't taking subscribers yet, and the dashboard says so too. You can share the link anyway, and it opens once email is connected ([Connect Amazon SES](../get-started/
|
|
5
|
+
**Signups open only once an email provider is connected.** Until then, the page says the newsletter isn't taking subscribers yet, and the dashboard says so too. You can share the link anyway, and it opens once email is connected ([Connect Amazon SES](../get-started/05-ses.md)).
|
|
6
6
|
|
|
7
7
|
## How a reader joins
|
|
8
8
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Before your readers get anything, check three things yourself: your email arrives and replies reach you, subscribing works, and problems will reach you.
|
|
4
4
|
|
|
5
|
-
**Email has to be connected first.** Until it is, nothing can be sent, and the dashboard's checklist shows **Connect Amazon SES** in place of this step, as [Connect Amazon SES](../get-started/
|
|
5
|
+
**Email has to be connected first.** Until it is, nothing can be sent, and the dashboard's checklist shows **Connect Amazon SES** in place of this step, as [Connect Amazon SES](../get-started/05-ses.md) describes.
|
|
6
|
+
|
|
7
|
+
**Until AWS grants production access, Amazon SES mails only addresses verified in SES.** So test with a verified address, and hold the subscribe link until then. The checklist adds **Request production access** after this step, and checks it off once AWS grants it, as [Request production access](../get-started/06-production-access.md) describes.
|
|
6
8
|
|
|
7
9
|
## 1. Send yourself a test
|
|
8
10
|
|
|
@@ -10,7 +12,7 @@ Choose **Template** in the sidebar, then **Send test email**, and send a sample
|
|
|
10
12
|
|
|
11
13
|
- **The From name and address,** which readers see first. They're set when your instance is deployed.
|
|
12
14
|
- **Where it lands:** the inbox, Promotions, or spam. If it lands in spam, check your sending domain's records, as [Sending-domain DNS](../reference/02-sending-domain-dns.md) describes.
|
|
13
|
-
- **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/
|
|
15
|
+
- **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/05-ses.md#8-receive-replies-at-your-from-address).
|
|
14
16
|
|
|
15
17
|
## 2. Subscribe yourself
|
|
16
18
|
|
|
@@ -10,11 +10,11 @@ In local development, `npm run dev` reads the top level of `wrangler.jsonc` and
|
|
|
10
10
|
|
|
11
11
|
None of these can be read or changed from inside the app, by the editor or by Claude. Settings in the editor shows the public ones, read-only.
|
|
12
12
|
|
|
13
|
-
This is the `production` block a new instance starts with
|
|
13
|
+
This is the `production` block a new instance starts with, here for an instance named `newsletter`. [Deploy the app](../get-started/02-deploy.md) fills in its database and hostnames:
|
|
14
14
|
|
|
15
15
|
```jsonc
|
|
16
16
|
"production": {
|
|
17
|
-
"name": "
|
|
17
|
+
"name": "newsletter-production",
|
|
18
18
|
"routes": [{ "pattern": "newsletter.example.com", "custom_domain": true }],
|
|
19
19
|
"workers_dev": false,
|
|
20
20
|
"preview_urls": false,
|
|
@@ -23,12 +23,12 @@ This is the `production` block a new instance starts with. [Deploy the app](../g
|
|
|
23
23
|
"d1_databases": [
|
|
24
24
|
{
|
|
25
25
|
"binding": "DB",
|
|
26
|
-
"database_name": "
|
|
26
|
+
"database_name": "newsletter-production",
|
|
27
27
|
"database_id": "REPLACE_WITH_PRODUCTION_D1_ID",
|
|
28
28
|
"migrations_dir": "migrations"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
-
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "
|
|
31
|
+
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "newsletter-media-production" }],
|
|
32
32
|
"vars": {
|
|
33
33
|
"PROVIDER": "",
|
|
34
34
|
"APP_ORIGIN": "https://newsletter.example.com",
|
|
@@ -46,9 +46,9 @@ Vars go in the environment block's `vars`. Every value is a string.
|
|
|
46
46
|
|
|
47
47
|
- `PROVIDER` optional
|
|
48
48
|
|
|
49
|
-
The email provider: `ses`, or `fake` for local development. Default: none, so no provider is connected. Refused unless it's exactly `ses`, `fake`, or `none`: a misspelling, or `SES`, is never taken for another. `resend`, which earlier releases supported, is refused with a pointer to [Connect Amazon SES](../get-started/
|
|
49
|
+
The email provider: `ses`, or `fake` for local development. Default: none, so no provider is connected. Refused unless it's exactly `ses`, `fake`, or `none`: a misspelling, or `SES`, is never taken for another. `resend`, which earlier releases supported, is refused with a pointer to [Connect Amazon SES](../get-started/05-ses.md).
|
|
50
50
|
|
|
51
|
-
Until it's set, or while it's empty, the app runs with no provider connected. Posts can be written and previewed, but everything that would send email is refused: scheduling, sending now, test emails, and the confirmation emails that signups need. The subscribe page says signups aren't open yet, and Settings shows **No email provider connected**. Setting it to `none` means the same. A new instance's `production` block declares it empty, so wrangler doesn't warn that the top level's value is missing there. [Connect Amazon SES](../get-started/
|
|
51
|
+
Until it's set, or while it's empty, the app runs with no provider connected. Posts can be written and previewed, but everything that would send email is refused: scheduling, sending now, test emails, and the confirmation emails that signups need. The subscribe page says signups aren't open yet, and Settings shows **No email provider connected**. Setting it to `none` means the same. A new instance's `production` block declares it empty, so wrangler doesn't warn that the top level's value is missing there. [Connect Amazon SES](../get-started/05-ses.md) changes it to `ses`.
|
|
52
52
|
|
|
53
53
|
Change it, or move to another SES account or region, only while no send is sending. The dashboard lists any send in progress.
|
|
54
54
|
|
|
@@ -64,7 +64,7 @@ Vars go in the environment block's `vars`. Every value is a string.
|
|
|
64
64
|
|
|
65
65
|
- `AWS_REGION` required with SES
|
|
66
66
|
|
|
67
|
-
The SES region. The template sets `us-east-1`.
|
|
67
|
+
The SES region. The template sets `us-east-1`. SES keeps identities, quotas, and the sandbox separately in each region, so set the region where you verified your sending hostname. On an AWS page, go by the region menu at the top right, not the page's address. A page for a global service, such as IAM or Billing, has no region to choose, so open SES before you choose. A few newer regions must be [enabled for your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-regions.html) first.
|
|
68
68
|
|
|
69
69
|
- `FROM_ADDRESS` required with a real provider
|
|
70
70
|
|
|
@@ -84,9 +84,9 @@ Vars go in the environment block's `vars`. Every value is a string.
|
|
|
84
84
|
|
|
85
85
|
- `SES_MAX_SEND_RATE` optional
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
A cap on the send rate, in messages a second. Usually left unset: the app reads your SES account's maximum send rate itself, about every ten minutes while nothing is sending, and keeps to the lower of the two when both are known. Without either, it sends at `14`, a common starting rate, not your account's. Refused unless it's a whole number above zero. **Settings → Email sender** shows the rate a send keeps to, and where it comes from.
|
|
88
88
|
|
|
89
|
-
The app sends SES one request a second, the most SES takes, and each request carries one second of
|
|
89
|
+
The app sends SES one request a second, the most SES takes, and each request carries one second of the rate, up to 50 recipients. Set this only to send slower than your account allows, or when the app's key can't read the account (it needs `ses:GetAccount`). To send faster, ask AWS to raise that rate to 50 before you move to Workers Paid: on Workers Free, a rate of 50 sends nearly four times as many recipients a minute as 14, and a higher rate adds nothing there. A rate set too high only means SES throttles some requests. Those recipients wait for the next minute, and no one is mailed twice.
|
|
90
90
|
|
|
91
91
|
- `SUBREQUEST_BUDGET` optional
|
|
92
92
|
|
|
@@ -162,7 +162,9 @@ Wrangler doesn't carry bindings or vars over from the top level of `wrangler.jso
|
|
|
162
162
|
|
|
163
163
|
- `name` required
|
|
164
164
|
|
|
165
|
-
The Worker's name,
|
|
165
|
+
The Worker's name. `kestrel init` names the Worker, the database, and the bucket after the instance. The instance's name is its folder's name, made to fit the rule below (`My Newsletter` gives `my-newsletter`, and `newsletter.example.com` gives `newsletter-example-com`), or the name given with `init`'s `--name`. An instance named `newsletter` deploys as `newsletter-production`, with the database of the same name and the bucket `newsletter-media-production`, and the top level's development Worker is `newsletter-dev`. A name takes lowercase letters, digits, and hyphens, starts and ends with a letter or digit, and is at most 46 characters, so every name made from it fits Cloudflare's rules. It can't be `kestrel`, which instances made by earlier releases use.
|
|
166
|
+
|
|
167
|
+
Each instance in a Cloudflare account needs a name of its own, since a deploy under another instance's Worker name replaces that Worker. An instance made by an earlier release keeps its names, `kestrel-production` and `kestrel-media-production`, and needs no change.
|
|
166
168
|
|
|
167
169
|
- `routes` required
|
|
168
170
|
|
|
@@ -178,7 +180,7 @@ Wrangler doesn't carry bindings or vars over from the top level of `wrangler.jso
|
|
|
178
180
|
|
|
179
181
|
- `observability` optional
|
|
180
182
|
|
|
181
|
-
Workers Logs, where the app's log lines land. On in the template. Kestrel sets no sampling, since sampling could drop the one minute that reports a stuck or missed send, so leave `head_sampling_rate` at its default of 1. Cloudflare's own daily limit on the Free plan can still stop new logs for the rest of the day, as [Read the logs](../get-started/
|
|
183
|
+
Workers Logs, where the app's log lines land. On in the template. Kestrel sets no sampling, since sampling could drop the one minute that reports a stuck or missed send, so leave `head_sampling_rate` at its default of 1. Cloudflare's own daily limit on the Free plan can still stop new logs for the rest of the day, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes.
|
|
182
184
|
|
|
183
185
|
- `DB` required
|
|
184
186
|
|
|
@@ -194,7 +196,7 @@ Wrangler doesn't carry bindings or vars over from the top level of `wrangler.jso
|
|
|
194
196
|
|
|
195
197
|
## What the app refuses
|
|
196
198
|
|
|
197
|
-
The app checks its configuration on every request. It refuses to run on one that would do the wrong thing quietly, and each setting above says what it refuses. In local development, `SIMULATE_SENDS` must also be off (`off`, `0`, `false`, `no`, or empty), `not-connected` to run as a deployment with no email provider yet, or the provider a local send models: `ses` or `generic` (also `1`, `on`, `true`, or `yes`), optionally followed by `:none` for no injected failures or `:realistic`, the default. Case doesn't matter. A deployed instance ignores
|
|
199
|
+
The app checks its configuration on every request. It refuses to run on one that would do the wrong thing quietly, and each setting above says what it refuses. In local development, `SIMULATE_SENDS` must also be off (`off`, `0`, `false`, `no`, or empty), `not-connected` to run as a deployment with no email provider yet, or the provider a local send models: `ses` or `generic` (also `1`, `on`, `true`, or `yes`), optionally followed by `:none` for no injected failures or `:realistic`, the default. `SIMULATE_ACCOUNT` must be off, or, beside `SIMULATE_SENDS=ses`, the SES account state local development stands in for: `sandbox`, `pending`, `denied`, `production`, or `permission`, optionally followed by `:unrouted`. Case doesn't matter. A deployed instance ignores both.
|
|
198
200
|
|
|
199
201
|
Until it's fixed, no sends go out, and each place says what its reader can act on:
|
|
200
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sending-domain DNS: SPF, DKIM, and DMARC
|
|
2
2
|
|
|
3
|
-
Inboxes decide whether to trust your mail by checking records in the DNS of the domain it comes from. This page explains the three checks they run: SPF, DKIM, and DMARC. It covers the records [Connect Amazon SES](../get-started/
|
|
3
|
+
Inboxes decide whether to trust your mail by checking records in the DNS of the domain it comes from. This page explains the three checks they run: SPF, DKIM, and DMARC. It covers the records [Connect Amazon SES](../get-started/05-ses.md) has you add, what each proves, how to check them, and when to tighten DMARC.
|
|
4
4
|
|
|
5
5
|
## Why all three
|
|
6
6
|
|
|
@@ -27,13 +27,13 @@ SES generates the DKIM values. Copy them exactly as SES lists them, since they d
|
|
|
27
27
|
|
|
28
28
|
SPF needs no record of yours on SES's default path, since it passes for a subdomain of `amazonses.com`. An SPF record, at `bounce.send`, comes with the custom MAIL FROM domain.
|
|
29
29
|
|
|
30
|
-
Email Routing adds MX, SPF, and DKIM records at `send` itself, the DKIM one under the selector `cf2024-1`. They're for mail sent *to* your From address, so readers' replies reach the publisher ([Connect Amazon SES](../get-started/
|
|
30
|
+
Email Routing adds MX, SPF, and DKIM records at `send` itself, the DKIM one under the selector `cf2024-1`. They're for mail sent *to* your From address, so readers' replies reach the publisher ([Connect Amazon SES](../get-started/05-ses.md#8-receive-replies-at-your-from-address)). They don't affect your sending: inboxes check SES's mail for SPF at its return path, and for DKIM against SES's own keys.
|
|
31
31
|
|
|
32
32
|
### SPF: authorize the return path
|
|
33
33
|
|
|
34
34
|
SPF checks the message's return path, the address bounces go back to, not its `From:` header. DMARC passes when SPF or DKIM passes and aligns with the `From:` domain. DKIM aligns, so an aligned SPF is a second check in your favor, not a requirement.
|
|
35
35
|
|
|
36
|
-
SES uses a subdomain of its own domain, `amazonses.com`, as the return path by default. SPF then passes for it but doesn't align, and DMARC passes through DKIM alone. A custom MAIL FROM domain, such as `bounce.send.example.com`, aligns SPF too. [Connect Amazon SES](../get-started/
|
|
36
|
+
SES uses a subdomain of its own domain, `amazonses.com`, as the return path by default. SPF then passes for it but doesn't align, and DMARC passes through DKIM alone. A custom MAIL FROM domain, such as `bounce.send.example.com`, aligns SPF too. [Connect Amazon SES](../get-started/05-ses.md#1-verify-your-sending-hostname) sets one up, with an MX record that names your region and an SPF record:
|
|
37
37
|
|
|
38
38
|
```
|
|
39
39
|
bounce.send.example.com. MX 10 feedback-smtp.us-east-1.amazonses.com.
|
|
@@ -67,7 +67,7 @@ Once the reports show your sends passing and aligned, tighten the policy to `p=q
|
|
|
67
67
|
|
|
68
68
|
## Verify alignment
|
|
69
69
|
|
|
70
|
-
A test email proves alignment better than any DNS lookup. Send yourself one, as in [Verify it works](../get-started/
|
|
70
|
+
A test email proves alignment better than any DNS lookup. Send yourself one, as in [Verify it works](../get-started/07-verify.md#2-check-that-it-passes-authentication), and read its `Authentication-Results` header. In Gmail, open the message's menu, and select **Show original**. Expect:
|
|
71
71
|
|
|
72
72
|
- `dkim=pass` for `send.example.com`.
|
|
73
73
|
- `dmarc=pass` for `send.example.com`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- 0015_provider_account: what the provider's account last said about itself (SPEC §9).
|
|
2
|
+
--
|
|
3
|
+
-- One row, written by the sweep's account check: when it last asked (`attempted_at`), what came
|
|
4
|
+
-- of it (`outcome`: `ok`, `permission` for a key not allowed to ask, `unreachable` for no usable
|
|
5
|
+
-- answer), and the last answer it got (`answer`, JSON, with `checked_at`), which stands until a
|
|
6
|
+
-- newer one replaces it. It is an observation, not a preference, so it lives apart from the
|
|
7
|
+
-- settings row; nothing that decides whether to send reads it.
|
|
8
|
+
--
|
|
9
|
+
-- What can't be recovered: nothing. The table starts empty and fills on the first idle tick.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE provider_account (
|
|
12
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
13
|
+
attempted_at INTEGER NOT NULL,
|
|
14
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('ok', 'permission', 'unreachable')),
|
|
15
|
+
checked_at INTEGER,
|
|
16
|
+
answer TEXT CHECK (answer IS NULL OR json_valid(answer)),
|
|
17
|
+
CHECK ((checked_at IS NULL) = (answer IS NULL))
|
|
18
|
+
) STRICT;
|