@kurtbruns/kestrel 1.10.0 → 1.12.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/CHANGELOG.md +64 -1
- package/README.md +17 -28
- package/cli/reset.mjs +2 -0
- package/cli/seed.mjs +2 -0
- package/demo/README.md +4 -2
- package/demo/posts/0-test-email/index.md +13 -0
- package/demo/posts/5-a-quick-note/index.md +2 -0
- package/dist/public/dashboard/app.js +302 -293
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +434 -54
- package/dist/worker/index.js +1472 -470
- package/docs/README.md +2 -2
- package/docs/get-started/02-deploy.md +12 -5
- package/docs/get-started/03-access.md +2 -0
- package/docs/get-started/04-ses.md +36 -16
- package/docs/get-started/05-verify.md +36 -8
- package/docs/get-started/06-go-live.md +13 -55
- package/docs/guides/01-connect-claude.md +7 -5
- package/docs/guides/03-notifications.md +10 -10
- package/docs/guides/09-as-your-list-grows.md +62 -0
- package/docs/publishing/01-your-list.md +118 -47
- package/docs/publishing/02-sending-rules.md +5 -5
- package/docs/publishing/03-name-your-publication.md +7 -93
- package/docs/publishing/04-write-a-post.md +37 -66
- package/docs/publishing/06-troubleshooting.md +103 -0
- package/docs/publishing/07-working-with-claude.md +19 -37
- package/docs/publishing/09-share-your-subscribe-link.md +11 -59
- package/docs/publishing/10-check-your-setup.md +12 -30
- package/docs/publishing/11-email-template.md +34 -0
- package/docs/publishing/README.md +12 -11
- package/docs/reference/01-configuration.md +6 -6
- package/docs/reference/02-sending-domain-dns.md +1 -1
- package/migrations/0007_sends_progress_at.sql +12 -0
- package/migrations/0008_notifications_sandbox.sql +35 -0
- package/migrations/0009_post_email_only.sql +12 -0
- package/migrations/0010_unlisted_links.sql +43 -0
- package/migrations/0011_sends_render_warnings.sql +13 -0
- package/package.json +1 -1
- package/template/claude/CLAUDE.md +5 -2
- package/template/claude/skills/migrate-list/SKILL.md +2 -2
- package/docs/guides/08-try-locally.md +0 -108
- package/docs/publishing/05-scheduling-and-sending.md +0 -72
- package/docs/publishing/06-after-a-send.md +0 -109
- 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
|
|
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.
|
|
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
|
|
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.**
|
|
134
|
-
- **`ARCHIVE_BASE_PATH`** is permanent once
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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,20 @@ 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
|
|
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
|
|
108
|
+
- How many subscribers the newsletter expects, and how often it sends.
|
|
109
109
|
|
|
110
|
-
Your account stays in the sandbox until AWS grants access, so don't go live before then. In the sandbox,
|
|
110
|
+
Your account stays in the sandbox until AWS grants access, so don't go live before then. In the sandbox, SES refuses mail to any address you haven't verified:
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
- A signup from an unverified address gets no confirmation email. The app emails the publisher about it, at most once a day.
|
|
113
|
+
- A post goes to your verified addresses and then waits, with the line "The provider is refusing the account" and SES's words. No reader is marked unsent. Once AWS grants access, the post goes to the rest at its next retry, mailing no one twice.
|
|
114
|
+
- A daily quota refusal in the sandbox (200 emails a day) means the same thing: request production access rather than a higher quota.
|
|
115
|
+
|
|
116
|
+
When AWS grants access, the **Account dashboard** shows your daily quota and your maximum send rate. You set the rate in the app in section 8. The rate decides how fast a send goes out, and [As your list grows](../guides/09-as-your-list-grows.md) says when to ask AWS to raise it.
|
|
113
117
|
|
|
114
118
|
## 6. Send bounces and complaints to SNS
|
|
115
119
|
|
|
@@ -223,7 +227,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
|
|
|
223
227
|
}
|
|
224
228
|
```
|
|
225
229
|
|
|
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](
|
|
230
|
+
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
231
|
|
|
228
232
|
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
233
|
|
|
@@ -256,10 +260,26 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
|
|
|
256
260
|
|
|
257
261
|
1. Reload the subscription. Its status changes to **Confirmed**, with nothing for you to click.
|
|
258
262
|
|
|
263
|
+
1. Give SNS longer to retry. By default it retries a failed delivery 3 times over about a minute, then discards the event, so a bounce that arrives while the app is briefly down or mid-deploy is lost, and the address gets your next post. Open the topic, choose **Edit**, and expand **Delivery policy (HTTP/S)**. Clear **Use the default delivery policy**, set these, and choose **Save changes**:
|
|
264
|
+
|
|
265
|
+
| Field | Value |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| Number of retries | `13` |
|
|
268
|
+
| Retries without delay | `0` |
|
|
269
|
+
| Minimum delay | `20` |
|
|
270
|
+
| Minimum delay retries | `2` |
|
|
271
|
+
| Maximum delay | `300` |
|
|
272
|
+
| Maximum delay retries | `6` |
|
|
273
|
+
| Retry-backoff function | Exponential |
|
|
274
|
+
|
|
275
|
+
SNS then keeps trying for most of an hour, the longest it allows. It never retries an event the app refused. The app refuses only an event it can't verify came from your topic, and logs an error when one claims to be from SNS.
|
|
276
|
+
|
|
277
|
+
1. **(Optional)** Keep what SNS still can't deliver. On the subscription, choose **Edit**, and set a **Redrive policy (dead-letter queue)** to an SQS queue of your own. SNS then moves an event there instead of discarding it, where you can read it later. [Amazon SNS dead-letter queues](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html) covers the queue and its permissions.
|
|
278
|
+
|
|
259
279
|
If it stays **Pending confirmation**, the request never reached the app, or the app refused it. Check, in this order:
|
|
260
280
|
|
|
261
281
|
- **Access covers `/webhooks/ses`.** If your Access application covers more than `/dashboard` and `/api`, SNS gets the login page instead of the app. Remove that path from your Access application, so only `dashboard` and `api` remain.
|
|
262
|
-
- **`SNS_TOPIC_ARN` matches the topic's ARN.**
|
|
282
|
+
- **`SNS_TOPIC_ARN` matches the topic's ARN.** If it doesn't, the app's logs show a `webhook.refused` error naming both.
|
|
263
283
|
- **The deploy in section 8 finished.**
|
|
264
284
|
|
|
265
285
|
Once you've fixed the cause, select the subscription, and choose **Request confirmation**. SNS sends the request again, and the app confirms it. SNS doesn't let you delete a subscription while it's pending, and removes it on its own after 48 hours.
|
|
@@ -310,7 +330,7 @@ Kestrel can't tell whether those emails went out. SES's console shows sent messa
|
|
|
310
330
|
dig +short MX send.example.com
|
|
311
331
|
```
|
|
312
332
|
|
|
313
|
-
It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows
|
|
333
|
+
It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows the publisher's inbox as verified. A reply to your test email in the next step proves the forwarding works.
|
|
314
334
|
|
|
315
335
|
1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before.
|
|
316
336
|
|
|
@@ -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
|
|
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
|
|
5
|
+
In this guide, you send yourself a test email, reply to it, and check that it passes authentication. Then you set up notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, read the app's logs, check that SES can send to anyone, and clean up the test subscribers.
|
|
6
6
|
|
|
7
7
|
## Before you begin
|
|
8
8
|
|
|
@@ -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
|
|
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
|
|
40
|
+
## 3. Set up notifications
|
|
41
41
|
|
|
42
|
-
Kestrel emails
|
|
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,12 +57,16 @@ 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
|
|
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.
|
|
61
|
+
|
|
62
|
+
1. Uncheck **Include in archive**. The post still goes to subscribers, but it's sent as email only: it isn't in the public archive or on the home page. That way the test proves the send without becoming the publication's first archived post, and the dashboard keeps its **Finish setting up** steps for the publisher.
|
|
59
63
|
|
|
60
|
-
1. Cancel the send, to see that canceling stops it. Then select **Send now** again, and let it go.
|
|
64
|
+
1. Cancel the send, to see that canceling stops it. Then select **Schedule** and **Send now** again, check that **Include in archive** is still unchecked, and let it go.
|
|
61
65
|
|
|
62
66
|
1. When the send finishes, the post arrives at your second address. A notification arrives at your first, with the subject `Sent:` and the post's title.
|
|
63
67
|
|
|
68
|
+
If you verified this instance before Email only existed, the test post is in the archive. Open it under **Sent**, and select **Remove from archive…**.
|
|
69
|
+
|
|
64
70
|
## 5. Unsubscribe
|
|
65
71
|
|
|
66
72
|
Every email carries an unsubscribe link, and a header that lets mail clients show their own **Unsubscribe** button. Both work in one step, with no login.
|
|
@@ -86,7 +92,7 @@ If an address has no flag, SES's events aren't reaching the app. Check the confi
|
|
|
86
92
|
|
|
87
93
|
## 7. Read the logs
|
|
88
94
|
|
|
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
|
|
95
|
+
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
96
|
|
|
91
97
|
1. In the Cloudflare dashboard, go to **Workers & Pages**, select `kestrel-production`, and select **Observability**.
|
|
92
98
|
|
|
@@ -96,6 +102,28 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
|
|
|
96
102
|
|
|
97
103
|
1. Filter on `level` equal to `error`. A healthy instance shows nothing.
|
|
98
104
|
|
|
105
|
+
## 8. Check that SES can send to anyone
|
|
106
|
+
|
|
107
|
+
[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.
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
## 9. Clean up the test subscribers
|
|
116
|
+
|
|
117
|
+
The checks leave test addresses on the list. Erase them, so the publisher starts with only real readers.
|
|
118
|
+
|
|
119
|
+
1. In the editor, open **Subscribers**.
|
|
120
|
+
|
|
121
|
+
1. Erase `bounce@simulator.amazonses.com` and `complaint@simulator.amazonses.com`. Open each one's menu (⋯), and select **Erase**.
|
|
122
|
+
|
|
123
|
+
1. Erase your second address too, if it won't be a reader.
|
|
124
|
+
|
|
125
|
+
Erasing keeps a do-not-contact marker, but the person can subscribe again through the subscribe page. Erasing your own address doesn't lock you out.
|
|
126
|
+
|
|
99
127
|
When every section gives the result it describes, your instance is ready for subscribers.
|
|
100
128
|
|
|
101
129
|
Next, [go live](06-go-live.md).
|
|
@@ -1,67 +1,25 @@
|
|
|
1
1
|
# Go live
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Start publishing
|
|
6
6
|
|
|
7
|
-
|
|
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).
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
1. Send them the editor's address and a link to [Name your publication](../publishing/03-name-your-publication.md).
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
## Optional next steps
|
|
19
18
|
|
|
20
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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,19 +77,21 @@ 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
|
|
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
|
|
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
|
|
|
86
86
|
1. Ask Claude to call `GET /api/whoami`. The app answers with a program's identity, not yours:
|
|
87
87
|
|
|
88
88
|
```
|
|
89
|
-
{"principal":{"kind":"service"},"auth":{"mode":"access"}}
|
|
89
|
+
{"principal":{"kind":"service","tokenId":"e367c0ffee5eb4.access"},"auth":{"mode":"access"}}
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
The `tokenId` is the Client ID Zero Trust shows beside `kestrel-claude`. If it's a different one, Claude Code is using another token.
|
|
93
|
+
|
|
94
|
+
1. Ask Claude to list the posts. It answers with the posts the editor shows.
|
|
93
95
|
|
|
94
96
|
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
97
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Notifications through Cloudflare's email
|
|
2
2
|
|
|
3
|
-
Kestrel emails
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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"] } // ←
|
|
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
|
|
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
|
|
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
|
|
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."
|