@kurtbruns/kestrel 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dev.vars.example +10 -0
- package/CHANGELOG.md +38 -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 +5607 -5096
- package/docs/README.md +5 -3
- package/docs/get-started/02-deploy.md +25 -23
- package/docs/get-started/03-access.md +13 -7
- package/docs/get-started/04-aws-account.md +68 -0
- package/docs/get-started/05-ses.md +307 -0
- package/docs/get-started/06-production-access.md +64 -0
- package/docs/get-started/07-verify.md +121 -0
- package/docs/get-started/{06-go-live.md → 08-go-live.md} +2 -2
- package/docs/guides/01-allow-claude.md +2 -2
- package/docs/guides/02-automate.md +1 -1
- package/docs/guides/03-notifications.md +4 -4
- 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 +3 -3
- package/docs/publishing/03-name-your-publication.md +3 -1
- package/docs/publishing/04-write-a-post.md +1 -1
- package/docs/publishing/06-troubleshooting.md +33 -68
- 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 +17 -13
- package/docs/reference/02-sending-domain-dns.md +5 -5
- 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
- package/docs/get-started/04-ses.md +0 -343
- package/docs/get-started/05-verify.md +0 -135
package/docs/README.md
CHANGED
|
@@ -8,9 +8,11 @@ These are the steps to deploy a Kestrel instance and get it ready to send. Each
|
|
|
8
8
|
|
|
9
9
|
1. [Deploy the app](get-started/02-deploy.md)
|
|
10
10
|
1. [Lock the dashboard with Access](get-started/03-access.md)
|
|
11
|
-
1. [
|
|
12
|
-
1. [
|
|
13
|
-
1. [
|
|
11
|
+
1. [Create an AWS account](get-started/04-aws-account.md)
|
|
12
|
+
1. [Connect Amazon SES](get-started/05-ses.md)
|
|
13
|
+
1. [Request production access](get-started/06-production-access.md)
|
|
14
|
+
1. [Verify it works](get-started/07-verify.md)
|
|
15
|
+
1. [Go live](get-started/08-go-live.md)
|
|
14
16
|
|
|
15
17
|
## Guides
|
|
16
18
|
|
|
@@ -9,7 +9,7 @@ In this guide, you choose your hostnames, create your instance's repository, cre
|
|
|
9
9
|
- A [Cloudflare account](https://dash.cloudflare.com/sign-up), with your domain's DNS on Cloudflare. Your app's hostname and its login both live there.
|
|
10
10
|
- [Node.js](https://nodejs.org/) 22 or later, and [Git](https://git-scm.com/).
|
|
11
11
|
- A GitHub account, or another Git host, for your instance's repository.
|
|
12
|
-
- An [AWS account](
|
|
12
|
+
- An AWS account for the newsletter, before [Connect Amazon SES](05-ses.md). [Create an AWS account](04-aws-account.md) walks through it. You can start it now. Signing up needs an email address that receives mail, and AWS can take a while to activate a new account.
|
|
13
13
|
|
|
14
14
|
## Choose your hostnames
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ Kestrel uses two hostnames: one for the app, and one your email is sent from.
|
|
|
22
22
|
|
|
23
23
|
These guides put the app on a subdomain, which leaves any website you already have on `example.com` untouched. If `example.com` has no website of its own, the app can live there instead. Use `example.com` wherever these guides say `newsletter.example.com`.
|
|
24
24
|
|
|
25
|
-
It's recommended to send from a subdomain rather than from `example.com` itself. Your apex domain carries the reputation of your regular email, and a newsletter shouldn't put that at risk. Avoid `mail.` as the sending name too, since mail servers read it as an inbound host. [Sending-domain DNS](../reference/02-sending-domain-dns.md#choose-your-sending-name) explains both.
|
|
25
|
+
It's recommended to send from a subdomain rather than from `example.com` itself. Your apex domain carries the reputation of your regular email, and a newsletter shouldn't put that at risk. These guides assume a sending subdomain throughout. Avoid `mail.` as the sending name too, since mail servers read it as an inbound host. [Sending-domain DNS](../reference/02-sending-domain-dns.md#choose-your-sending-name) explains both.
|
|
26
26
|
|
|
27
27
|
Throughout these guides, replace the example names with your own, and any value written as `REPLACE_WITH_…`.
|
|
28
28
|
|
|
@@ -30,9 +30,9 @@ Throughout these guides, replace the example names with your own, and any value
|
|
|
30
30
|
|
|
31
31
|
Your instance's repository holds its configuration, its database migrations, and the release of Kestrel it runs. Kestrel itself installs from npm as `@kurtbruns/kestrel`, so the repository stays small, and an upgrade changes the version it names.
|
|
32
32
|
|
|
33
|
-
1. On GitHub, [create a new repository](https://github.com/new) named `newsletter`. It can be private. Leave it empty, with no README, license, or `.gitignore`.
|
|
33
|
+
1. On GitHub, [create a new repository](https://github.com/new) named after your publication. These guides use `newsletter`. It can be private. Leave it empty, with no README, license, or `.gitignore`.
|
|
34
34
|
|
|
35
|
-
1. Clone it, and write a new instance into it. Replace `YOUR_USERNAME` with your GitHub username:
|
|
35
|
+
1. Clone it, and write a new instance into it. Replace `YOUR_USERNAME` with your GitHub username, and `newsletter` with your repository's name:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
git clone https://github.com/YOUR_USERNAME/newsletter.git
|
|
@@ -48,14 +48,6 @@ Your instance's repository holds its configuration, its database migrations, and
|
|
|
48
48
|
npm install
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
1. Commit the new instance, and push it:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
git add -A
|
|
55
|
-
git commit -m "Start a Kestrel instance"
|
|
56
|
-
git push -u origin HEAD
|
|
57
|
-
```
|
|
58
|
-
|
|
59
51
|
1. **(Optional)** Try Kestrel locally before you deploy it. Copy the local settings, and start the app:
|
|
60
52
|
|
|
61
53
|
```bash
|
|
@@ -63,7 +55,15 @@ Your instance's repository holds its configuration, its database migrations, and
|
|
|
63
55
|
npm run dev
|
|
64
56
|
```
|
|
65
57
|
|
|
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. Press Ctrl+C to stop the server when you're done.
|
|
58
|
+
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. Press Ctrl+C to stop the server when you're done. `init` prints these steps too, in this order.
|
|
59
|
+
|
|
60
|
+
1. Commit the new instance, and push it. Your local settings and data stay out of the commit, since the instance's `.gitignore` lists them:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git add -A
|
|
64
|
+
git commit -m "Initialize Kestrel instance"
|
|
65
|
+
git push -u origin HEAD
|
|
66
|
+
```
|
|
67
67
|
|
|
68
68
|
## 2. Sign in to Cloudflare
|
|
69
69
|
|
|
@@ -87,16 +87,18 @@ Every command from here on acts on your Cloudflare account through the Wrangler
|
|
|
87
87
|
|
|
88
88
|
Each command creates the resource and writes it into the `production` block of `wrangler.jsonc`, under the binding the app already uses. Name `--env production` on each one, or Wrangler writes to the top level, which is local development.
|
|
89
89
|
|
|
90
|
+
The commands below use `newsletter`. Use your instance's names instead: `kestrel init` printed them, and they're in the `production` block of `wrangler.jsonc`.
|
|
91
|
+
|
|
90
92
|
1. Create the database:
|
|
91
93
|
|
|
92
94
|
```bash
|
|
93
|
-
npx wrangler d1 create
|
|
95
|
+
npx wrangler d1 create newsletter-production --env production --binding DB --update-config
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
1. Create the bucket for images:
|
|
97
99
|
|
|
98
100
|
```bash
|
|
99
|
-
npx wrangler r2 bucket create
|
|
101
|
+
npx wrangler r2 bucket create newsletter-media-production --env production --binding MEDIA --update-config
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
Each prints a snippet to add to your configuration. There's nothing to paste: `--update-config` has filled in the database's id, and the `MEDIA` binding already names the bucket. Wrangler also rewrites `wrangler.jsonc` in its own layout, with tabs and one value per line, and your settings stay.
|
|
@@ -118,7 +120,7 @@ Each prints a snippet to add to your configuration. There's nothing to paste: `-
|
|
|
118
120
|
"d1_databases": [
|
|
119
121
|
{
|
|
120
122
|
"binding": "DB",
|
|
121
|
-
"database_name": "
|
|
123
|
+
"database_name": "newsletter-production", // named by init; leave it
|
|
122
124
|
"database_id": "…", // already filled in by section 3; leave it
|
|
123
125
|
"migrations_dir": "migrations"
|
|
124
126
|
}
|
|
@@ -136,9 +138,9 @@ Each prints a snippet to add to your configuration. There's nothing to paste: `-
|
|
|
136
138
|
What each one does:
|
|
137
139
|
|
|
138
140
|
- `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.
|
|
139
|
-
- `PROVIDER` stays empty until [Connect Amazon SES](
|
|
141
|
+
- `PROVIDER` stays empty until [Connect Amazon SES](05-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.
|
|
140
142
|
- `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.
|
|
141
|
-
- `FROM_ADDRESS` is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`.
|
|
143
|
+
- `FROM_ADDRESS` is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`. The name before the `<` is what readers see as the sender. It also names the publication on its public pages, until the publisher sets a name under **Settings → Publication identity**. You can change the name at any time, and deploy again.
|
|
142
144
|
|
|
143
145
|
Change every `example.com` value, the route included. The app refuses to run while any of its settings still names `example.com`.
|
|
144
146
|
|
|
@@ -175,17 +177,15 @@ npx wrangler deploy --env production
|
|
|
175
177
|
|
|
176
178
|
This deploys the release your instance runs, with your settings. The output ends with the hostname the app now answers on. Deploying also registers the once-a-minute schedule that sends posts, which you can see under the Worker's **Triggers** tab in the Cloudflare dashboard.
|
|
177
179
|
|
|
178
|
-
Always name `--env production`, here and on every wrangler command that stores a secret. Without it, wrangler
|
|
180
|
+
Always name `--env production`, here and on every wrangler command that stores a secret. Without it, wrangler deploys the top level, local development, as a second Worker that answers nowhere and never sends. It takes the top level's `name` in `wrangler.jsonc`, such as `newsletter-dev`. If that happens, remove it by that name:
|
|
179
181
|
|
|
180
182
|
```bash
|
|
181
|
-
npx wrangler delete --name
|
|
183
|
+
npx wrangler delete --name newsletter-dev
|
|
182
184
|
```
|
|
183
185
|
|
|
184
|
-
Leave the top level's `"workers_dev": false`, `"preview_urls": false`, and `"triggers": { "crons": [] }` as they are, and never add a route there: they are what keep a deploy like that inert. Keep the empty `crons` rather than deleting the line, since only an empty list clears a schedule an earlier deploy left.
|
|
185
|
-
|
|
186
186
|
## Check it
|
|
187
187
|
|
|
188
|
-
1. The app answers on your hostname
|
|
188
|
+
1. The app answers on your hostname:
|
|
189
189
|
|
|
190
190
|
```bash
|
|
191
191
|
curl https://newsletter.example.com/health
|
|
@@ -195,6 +195,8 @@ Leave the top level's `"workers_dev": false`, `"preview_urls": false`, and `"tri
|
|
|
195
195
|
{"status":"ok","service":"kestrel"}
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
+
If `curl` reports a certificate error, or says it could not resolve the host, Cloudflare is still setting up the new hostname. Try again in a few minutes.
|
|
199
|
+
|
|
198
200
|
1. The editor is locked. Until you set up Access in the next step, the app can't tell who is asking, so it lets no one in:
|
|
199
201
|
|
|
200
202
|
```bash
|
|
@@ -37,7 +37,11 @@ The editor lives under `/dashboard`, and the API under `/api`. One application c
|
|
|
37
37
|
|
|
38
38
|
Each path covers everything under it. Leave every other path out. The home page, the archive, subscribing, unsubscribing, images, and your email provider's webhooks must stay public.
|
|
39
39
|
|
|
40
|
-
1.
|
|
40
|
+
1. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it, or saving fails with an error naming `use_clientless_isolation_app_launcher_url`.
|
|
41
|
+
|
|
42
|
+
1. Name the application after your app's hostname, `newsletter.example.com`. Cloudflare suggests a name made from the subdomain, which won't tell two newsletters apart.
|
|
43
|
+
|
|
44
|
+
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. For example, a week may suit a publisher on their own computer.
|
|
41
45
|
|
|
42
46
|
## 3. Allow yourself in
|
|
43
47
|
|
|
@@ -45,15 +49,13 @@ Every Access application denies everyone until a policy allows them.
|
|
|
45
49
|
|
|
46
50
|
1. Under **Access policies**, create a new policy:
|
|
47
51
|
|
|
48
|
-
- **Policy name:** `
|
|
52
|
+
- **Policy name:** the publication's name and `publishers`, such as `Example publishers`
|
|
49
53
|
- **Action:** Allow
|
|
50
54
|
- **Include:** the **Emails** selector, with your email address
|
|
51
55
|
|
|
52
56
|
1. Select **Create**.
|
|
53
57
|
|
|
54
|
-
Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in
|
|
55
|
-
|
|
56
|
-
The app accepts only two kinds of policy on this application. One is an Allow policy on people's emails, like this one. The other is a Service Auth policy on a service token, from [Automate with a service token](../guides/02-automate.md). A login through any other kind, such as a certificate, a device check, or an identity provider that gives no email, is refused with a `401`, and the editor says so.
|
|
58
|
+
Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made should be one. To let someone sign in another way, such as a one-time code by email, add it under **Zero Trust → Integrations → Identity providers**.
|
|
57
59
|
|
|
58
60
|
## 4. Give the app its two values
|
|
59
61
|
|
|
@@ -107,6 +109,10 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
107
109
|
{"status":"ok","service":"kestrel"}
|
|
108
110
|
```
|
|
109
111
|
|
|
110
|
-
If
|
|
112
|
+
If a check fails:
|
|
113
|
+
|
|
114
|
+
- **The editor says the configuration needs a fix.** Store the secret it names. While only one of the two is stored, `/health` answers `500` naming the other, and the public pages say the newsletter is temporarily unavailable.
|
|
115
|
+
- **Cloudflare lets you in, but the editor answers `401`.** Check the two secrets for a typo, and that `ACCESS_ALLOWED_EMAILS`, if you set it, includes your address.
|
|
116
|
+
- **The editor says Access let you in but didn't say who you are.** Your login carried no email, which happens through a kind of policy the app doesn't accept, such as a device check. Sign out, and sign in through the policy you made above.
|
|
111
117
|
|
|
112
|
-
Next, [
|
|
118
|
+
Next, [create an AWS account](04-aws-account.md).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Create an AWS account
|
|
2
|
+
|
|
3
|
+
Kestrel sends the newsletter's email through Amazon SES, which runs in an Amazon Web Services (AWS) account. This page assumes a brand-new account, made for the newsletter. If you'd rather use one you already have, skip to [Connect Amazon SES](05-ses.md).
|
|
4
|
+
|
|
5
|
+
If you plan to run multiple publications, you may want to consider setting up one account for each publication. With this setup, you can use [AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html) to keep the accounts under one bill. Multiple publications can be run through one account. However, they share its sandbox, sending quotas, and suppression list, and AWS can pause sending for the whole account if one publication's bounces or complaints run high.
|
|
6
|
+
|
|
7
|
+
In this guide, you choose the account's email address, sign up, and secure the account's root user. Then you set a monthly budget, so AWS emails you if the account ever costs more than you expect.
|
|
8
|
+
|
|
9
|
+
## Before you begin
|
|
10
|
+
|
|
11
|
+
- A phone number, and a credit or debit card for AWS's charges.
|
|
12
|
+
- Your domain's DNS on Cloudflare, if the account's email address will be forwarded with Cloudflare Email Routing.
|
|
13
|
+
|
|
14
|
+
## 1. Choose the account's email address
|
|
15
|
+
|
|
16
|
+
The address you sign up with belongs to the account's root user, the identity that owns the account and everything in it. You sign in as the root user with it, AWS contacts you there about the account, and it's how you recover the account. AWS recommends an address used for nothing else, which forwards to whoever looks after the account, as its [root user best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/root-user-best-practices.html) describe.
|
|
17
|
+
|
|
18
|
+
1. Choose an address named for its purpose, on your own domain, such as `aws@example.com`. Use one you'll keep, since recovering the account depends on it.
|
|
19
|
+
|
|
20
|
+
1. Make sure it receives mail before you sign up, since AWS emails it a code right away. If `example.com` receives no mail yet, forward the address to your inbox with Cloudflare Email Routing, as [Enable Email Routing](https://developers.cloudflare.com/email-routing/get-started/enable-email-routing/) describes. If `example.com` already receives mail at a mail host, create the address there instead, since Email Routing replaces a domain's mail servers.
|
|
21
|
+
|
|
22
|
+
## 2. Sign up
|
|
23
|
+
|
|
24
|
+
AWS may offer two ways to sign up. Choose **Sign up for AWS (advanced)**. The other way, **Sign up for AWS (new)**, makes an account with no root user, and chooses its region from your address. Its console differs from the one this guide describes. AWS's [Compare sign-up options](https://docs.aws.amazon.com/accounts/latest/reference/sign-up-for-aws.html) explains both.
|
|
25
|
+
|
|
26
|
+
1. Open [Sign up for AWS](https://signin.aws.amazon.com/signup?request_type=register). If it offers a choice, select **Sign up for AWS (advanced)**.
|
|
27
|
+
|
|
28
|
+
1. Enter the address from section 1 as the root user's email address, and an account name, such as your publication's name. Verify the address with the code AWS sends to it, and choose a password for the root user.
|
|
29
|
+
|
|
30
|
+
1. Choose the paid plan. The free plan closes the account automatically after six months, or sooner once its credits are used, unless you upgrade it before then. The paid plan gets the same sign-up credits, and keeps running once they're used. [AWS Free Tier plans](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier-plans.html) describes both.
|
|
31
|
+
|
|
32
|
+
1. Enter your contact information and payment details, and confirm your phone number if AWS asks.
|
|
33
|
+
|
|
34
|
+
1. Choose the free Basic support plan. It's enough for everything the setup guide does. Asking AWS to let SES send to anyone is a form in the SES console, with no paid support plan needed.
|
|
35
|
+
|
|
36
|
+
1. Complete the sign-up. The console opens, signed in as the root user, and AWS emails the account's address when the account is ready.
|
|
37
|
+
|
|
38
|
+
## 3. Secure the root user
|
|
39
|
+
|
|
40
|
+
The root user can do anything in the account, including close it, so it needs more than a password. AWS requires multi-factor authentication (MFA) for it, and asks you to set it up soon after your first sign-in.
|
|
41
|
+
|
|
42
|
+
1. In the console, select your account name at the top right, and then **Security credentials**.
|
|
43
|
+
|
|
44
|
+
1. Under **Multi-factor authentication (MFA)**, choose **Assign MFA device**. Enter a device name, select **Passkey or Security Key**, and follow the prompts. A passkey on your computer or phone works.
|
|
45
|
+
|
|
46
|
+
1. Leave **Access keys** empty. A root user's key could do anything the root user can. [Connect Amazon SES](05-ses.md#5-create-the-apps-aws-key) makes a separate key that can only send email and read the account's sending status.
|
|
47
|
+
|
|
48
|
+
## 4. Set a budget alert
|
|
49
|
+
|
|
50
|
+
AWS charges for what you use, with no spending limit, so a mistake or a leaked key could run up a bill before you notice. A budget emails you as the month's costs near an amount you choose. SES charges for each email it sends, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
|
|
51
|
+
|
|
52
|
+
1. In the console, open **Billing and Cost Management**, then **Budgets**, and choose **Create budget**.
|
|
53
|
+
|
|
54
|
+
1. Under **Use a template (simplified)**, select **Monthly cost budget**.
|
|
55
|
+
|
|
56
|
+
1. Enter a name, and an amount a little above what you expect to spend each month. Add the account's email address, or your own, as an email recipient. Choose **Create budget**.
|
|
57
|
+
|
|
58
|
+
1. AWS emails each recipient to verify it, and sends alerts only to a verified address. Open that email while you're signed in to the account, and verify the address within 12 hours, as [Budgets email recipients](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-email-recipients.html) describes.
|
|
59
|
+
|
|
60
|
+
## Check it
|
|
61
|
+
|
|
62
|
+
1. Sign out, and sign in again as the root user, with the address from section 1. After the password, AWS asks for your passkey or security key.
|
|
63
|
+
|
|
64
|
+
1. Under **Security credentials**, the MFA list shows your device, and **Access keys** lists none.
|
|
65
|
+
|
|
66
|
+
1. Under **Budgets**, your budget is listed, and its email recipient shows as **Active**.
|
|
67
|
+
|
|
68
|
+
Next, [connect Amazon SES](05-ses.md).
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Connect Amazon SES
|
|
2
|
+
|
|
3
|
+
This page connects the app to Amazon Simple Email Service (Amazon SES), which delivers the newsletter's email. A new SES account starts in a sandbox, where it can send only to addresses you've verified, until AWS reviews it. See [Request production access](06-production-access.md) to take it out of the sandbox.
|
|
4
|
+
|
|
5
|
+
In this guide, you set up how the app sends mail through SES and receives bounces and complaints, and how readers' replies reach the publisher.
|
|
6
|
+
|
|
7
|
+
## Before you begin
|
|
8
|
+
|
|
9
|
+
- An AWS account for the newsletter, from [Create an AWS account](04-aws-account.md).
|
|
10
|
+
- The publisher's inbox, for readers' replies.
|
|
11
|
+
|
|
12
|
+
## 1. Verify your sending hostname
|
|
13
|
+
|
|
14
|
+
SES sends only from an identity you've verified. Verifying the whole hostname lets any address on it send, and sets up DKIM, which lets inboxes trust your mail.
|
|
15
|
+
|
|
16
|
+
The same form sets a custom MAIL FROM domain, so SPF also passes for your domain. [SPF: authorize the return path](../reference/02-sending-domain-dns.md#spf-authorize-the-return-path) explains why it matters.
|
|
17
|
+
|
|
18
|
+
1. Open the [SES console](https://console.aws.amazon.com/ses/), and choose a region near you in the region menu at the top right.
|
|
19
|
+
|
|
20
|
+
SES keeps identities, quotas, and the sandbox separately in each region, so do every step on this page in the region you chose. [Configuration](../reference/01-configuration.md#vars) says more about choosing one, under `AWS_REGION`.
|
|
21
|
+
|
|
22
|
+
1. Note the region's code, such as `eu-west-1`. The menu shows it beside the region's name, such as Europe (Ireland), and [Amazon SES endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) lists each one too. Later steps use this code.
|
|
23
|
+
|
|
24
|
+
1. Under **Configuration**, choose **Identities**, then **Create identity**.
|
|
25
|
+
|
|
26
|
+
1. Select **Domain**, and enter `send.example.com`. Leave **Assign a default configuration set** and **Assign to a tenant** unchecked. Section 4 makes the configuration set, and makes it the default then.
|
|
27
|
+
|
|
28
|
+
1. Select **Use a custom MAIL FROM domain**. In **MAIL FROM domain**, enter `bounce`; the form shows `.send.example.com` after it. For **Behavior on MX failure**, keep **Use default MAIL FROM domain**.
|
|
29
|
+
|
|
30
|
+
1. Expand **Advanced DKIM settings**, and select **Easy DKIM**. Keep **RSA_2048_BIT** as the key length. Choose **Create identity**.
|
|
31
|
+
|
|
32
|
+
1. The identity opens on its **Authentication** tab, which lists five DNS records to publish. Under DKIM, three CNAME records. Under **Custom MAIL FROM domain**, expand **Publish DNS records** for an MX record and a TXT record.
|
|
33
|
+
|
|
34
|
+
1. Add all five to your domain's DNS records in Cloudflare, with their proxy status set to **DNS only**. SES can't read a proxied record, so check each one's status. You can import them in one go, or add them by hand:
|
|
35
|
+
|
|
36
|
+
- **Import them.** Copy this into a text file. Replace each `REPLACE_WITH_NAME_…` and `REPLACE_WITH_VALUE_…` with a DKIM record's name and value, exactly as SES lists them, and keep the dot after each. Replace `us-east-1` with your region's code. In the Cloudflare dashboard, open your domain's **DNS → Records**, select **Import and Export**, and import the file with **Proxy imported DNS records** unchecked. SES's own **Download .csv record set** is in a format Cloudflare doesn't import.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
REPLACE_WITH_NAME_1. 3600 IN CNAME REPLACE_WITH_VALUE_1.
|
|
40
|
+
REPLACE_WITH_NAME_2. 3600 IN CNAME REPLACE_WITH_VALUE_2.
|
|
41
|
+
REPLACE_WITH_NAME_3. 3600 IN CNAME REPLACE_WITH_VALUE_3.
|
|
42
|
+
bounce.send.example.com. 3600 IN MX 10 feedback-smtp.us-east-1.amazonses.com.
|
|
43
|
+
bounce.send.example.com. 3600 IN TXT "v=spf1 include:amazonses.com ~all"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Add them by hand.** Add each record exactly as SES lists it. Cloudflare adds your domain to the name, so each DKIM name ends in `._domainkey.send`, and the MAIL FROM records are named `bounce.send`.
|
|
47
|
+
|
|
48
|
+
1. Wait until SES shows **Identity status** as **Verified**, and both **DKIM configuration** and **MAIL FROM configuration** as **Successful**.
|
|
49
|
+
|
|
50
|
+
## 2. Add a DMARC record
|
|
51
|
+
|
|
52
|
+
DMARC tells inboxes what to do with mail that fails its checks, and sends you reports about it. SES doesn't add this record, so you add it yourself. It starts in monitor mode, which reports without blocking anything.
|
|
53
|
+
|
|
54
|
+
1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
|
|
55
|
+
|
|
56
|
+
- **Type:** `TXT`
|
|
57
|
+
- **Name:** `_dmarc.send`
|
|
58
|
+
- **Content:** `v=DMARC1; p=none; rua=mailto:dmarc@example.com`
|
|
59
|
+
|
|
60
|
+
1. Replace `dmarc@example.com` with an address on your own domain that receives mail, such as one forwarded to your inbox with Cloudflare Email Routing. Reports arrive there as attachments. An address on another domain, such as a Gmail address, gets no reports unless that domain publishes a record allowing them, which Gmail doesn't.
|
|
61
|
+
|
|
62
|
+
1. Check that the record is published. This prints it:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dig +short TXT _dmarc.send.example.com
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
[Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
|
|
69
|
+
|
|
70
|
+
## 3. Verify your test addresses
|
|
71
|
+
|
|
72
|
+
A new SES account starts in the sandbox: it sends only to addresses you've verified, up to 200 emails a day and one a second. The test emails in [Verify it works](07-verify.md) go to two addresses of your own, so verify those now. [Request production access](06-production-access.md) lifts the limit, once AWS grants it.
|
|
73
|
+
|
|
74
|
+
1. Choose two addresses you own, such as a personal address and a work address. Verify it works uses both.
|
|
75
|
+
|
|
76
|
+
1. For each, under **Identities**, choose **Create identity**, select **Email address**, and enter it. Choose **Create identity**.
|
|
77
|
+
|
|
78
|
+
1. Open the email AWS sends to each address, and follow its link. The identity's status changes to **Verified**.
|
|
79
|
+
|
|
80
|
+
## 4. Send bounces and complaints to the app
|
|
81
|
+
|
|
82
|
+
SES reports bounces and complaints through Amazon Simple Notification Service (SNS), which delivers them to the app. The app then stops mailing an address that bounced or complained. A configuration set says which events SES reports, and an SNS topic carries them.
|
|
83
|
+
|
|
84
|
+
1. In SES, under **Configuration**, choose **Configuration sets**, then **Create set**. Name it `kestrel-mail`, and choose **Create set**.
|
|
85
|
+
|
|
86
|
+
1. On the set's **Event destinations** tab, choose **Add destination**. Select **Hard bounces**, **Complaints**, **Deliveries**, and **Rendering failures**, and choose **Next**. The app logs a rendering failure, an email SES couldn't build, as an error.
|
|
87
|
+
|
|
88
|
+
1. For **Destination type**, select **Amazon SNS**. Name it `kestrel-sns`, and keep **Event publishing** enabled. Choose **Next**.
|
|
89
|
+
|
|
90
|
+
1. For **SNS topic**, choose **Create SNS topic**. Make it a **Standard** topic named `kestrel-events`. Choose **Next**, then **Add destination**.
|
|
91
|
+
|
|
92
|
+
1. Make the set your hostname's default, so SES reports on every email sent from it. Open the `send.example.com` identity, and choose the **Configuration set** tab. Under **Default configuration set**, choose **Edit**, select `kestrel-mail`, and save.
|
|
93
|
+
|
|
94
|
+
1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), in the same region, and find its ARN. It looks like `arn:aws:sns:us-east-1:111122223333:kestrel-events`, where `us-east-1` is your region's code and `111122223333` is your account ID.
|
|
95
|
+
|
|
96
|
+
1. Let SES publish to the topic. Choose **Edit**, expand **Access policy**, and add this statement to the `Statement` list:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"Effect": "Allow",
|
|
101
|
+
"Principal": { "Service": "ses.amazonaws.com" },
|
|
102
|
+
"Action": "sns:Publish",
|
|
103
|
+
"Resource": "REPLACE_WITH_TOPIC_ARN",
|
|
104
|
+
"Condition": {
|
|
105
|
+
"StringEquals": {
|
|
106
|
+
"AWS:SourceAccount": "REPLACE_WITH_ACCOUNT_ID",
|
|
107
|
+
"AWS:SourceArn": "arn:aws:ses:us-east-1:REPLACE_WITH_ACCOUNT_ID:configuration-set/kestrel-mail"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Replace the topic ARN, both copies of the account ID, and `us-east-1` with the values from your ARN. Choose **Save changes**.
|
|
114
|
+
|
|
115
|
+
1. Store the topic's ARN in the app. In your instance's repository, run this, and paste the ARN at the prompt. The app accepts events only from this topic:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx wrangler secret put SNS_TOPIC_ARN --env production
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
1. Store the configuration set's name, `kestrel-mail`. It's optional but recommended: the app names the set on every email, so SES reports on each one even without the hostname's default. Type it exactly as the set is named in this region, since SES refuses every email that names a set it can't find:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx wrangler secret put SES_CONFIGURATION_SET --env production
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 5. Create the app's AWS key
|
|
128
|
+
|
|
129
|
+
The app sends through an AWS Identity and Access Management (IAM) user whose key can only send email and read the account's sending status. That limits the harm if the key ever leaks. Reading the status lets the app show whether SES is still in the sandbox, take its send rate from the account, and warn when bounces and complaints can't reach it.
|
|
130
|
+
|
|
131
|
+
If `kestrel-sender` already exists, as on an instance set up before Kestrel read the account, don't create a second user or key. In the IAM console, open **Policies**, then `kestrel-send-email`, and choose **Edit**. In the JSON editor, add `"ses:GetAccount"` and `"ses:GetEmailIdentity"` to its `Action` list, as in the policy below, and save. The app's next check, within an hour, reads the account.
|
|
132
|
+
|
|
133
|
+
1. In the [IAM console](https://console.aws.amazon.com/iam/), go to **Users**, and choose **Create user**. Name it `kestrel-sender`, and leave console access off.
|
|
134
|
+
|
|
135
|
+
1. When asked for permissions, choose **Attach policies directly**, then **Create policy**. The policy editor opens in a new browser tab.
|
|
136
|
+
|
|
137
|
+
1. In the new tab, choose the **JSON** editor, and replace its contents with this policy:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"Version": "2012-10-17",
|
|
142
|
+
"Statement": [
|
|
143
|
+
{
|
|
144
|
+
"Effect": "Allow",
|
|
145
|
+
"Action": [
|
|
146
|
+
"ses:SendEmail",
|
|
147
|
+
"ses:SendBulkEmail",
|
|
148
|
+
"ses:GetAccount",
|
|
149
|
+
"ses:GetEmailIdentity"
|
|
150
|
+
],
|
|
151
|
+
"Resource": "*"
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Choose **Next**, name the policy `kestrel-send-email`, and choose **Create policy**. You can close this tab.
|
|
158
|
+
|
|
159
|
+
1. Go back to the first tab, where you're creating the user. Refresh the policy list, search for `kestrel-send-email`, and select it.
|
|
160
|
+
|
|
161
|
+
1. Choose **Next**, then **Create user**.
|
|
162
|
+
|
|
163
|
+
1. Open the user, and on the **Security credentials** tab, choose **Create access key**. For the use case, choose **Application running outside AWS**.
|
|
164
|
+
|
|
165
|
+
1. Keep the page open: AWS shows the secret access key only once. Store the access key ID in the app, and paste it at the prompt:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npx wrangler secret put AWS_ACCESS_KEY_ID --env production
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
1. Store the secret access key:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npx wrangler secret put AWS_SECRET_ACCESS_KEY --env production
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 6. Connect the app and deploy
|
|
178
|
+
|
|
179
|
+
Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` to `ses` connects SES, with the secrets you stored in sections 4 and 5. If a required one is missing, the app refuses to run until you store it, and `/health` names it.
|
|
180
|
+
|
|
181
|
+
1. In `wrangler.jsonc`, edit the `production` block's `vars`. Change `PROVIDER` to `ses` (it's `""` on a new instance; add it if your block has none). Change `AWS_REGION` to your region's code from section 1. Your other values stay as they are. The block ends up like this:
|
|
182
|
+
|
|
183
|
+
```jsonc
|
|
184
|
+
"vars": {
|
|
185
|
+
"PROVIDER": "ses", // ← set to ses
|
|
186
|
+
"APP_ORIGIN": "https://newsletter.example.com",
|
|
187
|
+
"ARCHIVE_BASE_PATH": "/archive",
|
|
188
|
+
"SENDING_DOMAIN": "send.example.com",
|
|
189
|
+
"FROM_ADDRESS": "Newsletter <newsletter@send.example.com>",
|
|
190
|
+
"AWS_REGION": "us-east-1" // ← change: your region's code
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
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 Workers Paid is worth it: for faster sends at any size, and for the daily database writes of a list past about 1,000 subscribers.
|
|
195
|
+
|
|
196
|
+
The app reads its send rate from your SES account, so there's nothing to change when AWS grants production access. **Settings → Email sender** shows the rate and where it comes from.
|
|
197
|
+
|
|
198
|
+
1. Commit the change, and push it:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git commit -am "Send through Amazon SES"
|
|
202
|
+
git push
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
1. Deploy:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npx wrangler deploy --env production
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
1. Check that the app answers on your hostname:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
curl https://newsletter.example.com/health
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
{"status":"ok","service":"kestrel"}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Any other answer names the setting the app is missing.
|
|
222
|
+
|
|
223
|
+
1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before. Within a couple of minutes, a **Sandbox** pill appears beside it, once the app has read your SES account.
|
|
224
|
+
|
|
225
|
+
1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
|
|
226
|
+
|
|
227
|
+
## 7. Subscribe the app to the topic
|
|
228
|
+
|
|
229
|
+
SNS delivers events only to a subscriber that has confirmed. The app confirms on its own, once it checks that the request came from your topic.
|
|
230
|
+
|
|
231
|
+
1. Check that the app's webhook is public. It refuses a request that isn't from SNS:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://newsletter.example.com/webhooks/ses
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
400
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Any other answer, such as a redirect to your Cloudflare login, means Access covers the webhook. Remove that path from your Access application.
|
|
242
|
+
|
|
243
|
+
1. In the SNS console, open your topic, and choose **Create subscription**.
|
|
244
|
+
|
|
245
|
+
1. Set **Protocol** to **HTTPS**, and enter the endpoint:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
https://newsletter.example.com/webhooks/ses
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
1. Leave raw message delivery off, and choose **Create subscription**.
|
|
252
|
+
|
|
253
|
+
1. Reload the subscription. Its status changes to **Confirmed**, with nothing for you to click.
|
|
254
|
+
|
|
255
|
+
If it stays **Pending confirmation**, check that your Access application covers only `/dashboard` and `/api`, that `SNS_TOPIC_ARN` matches the topic's ARN, and that section 6's deploy finished. A mismatched ARN shows in the app's logs as a `webhook.refused` error naming both. Then request confirmation again from the topic's subscriptions. SNS doesn't let you delete a pending subscription, and removes it on its own after 48 hours.
|
|
256
|
+
|
|
257
|
+
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 mid-deploy is lost. Open the topic, choose **Edit**, and expand **Delivery retry policy (HTTP/S)**. Turn off the default policy, set these values, and choose **Save changes**:
|
|
258
|
+
|
|
259
|
+
| Field | Value |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| Number of retries | `13` |
|
|
262
|
+
| Retries without delay | `0` |
|
|
263
|
+
| Minimum delay | `20` |
|
|
264
|
+
| Minimum delay retries | `2` |
|
|
265
|
+
| Maximum delay | `300` |
|
|
266
|
+
| Maximum delay retries | `6` |
|
|
267
|
+
| Retry-backoff function | Exponential |
|
|
268
|
+
|
|
269
|
+
SNS then keeps trying for about 40 minutes. It allows an hour at most.
|
|
270
|
+
|
|
271
|
+
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. [Amazon SNS dead-letter queues](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html) covers the queue and its permissions.
|
|
272
|
+
|
|
273
|
+
## 8. Receive replies at your From address
|
|
274
|
+
|
|
275
|
+
Replies to a post go to your From address, `newsletter@send.example.com`, which has no inbox yet, so they bounce. This section forwards that address to the publisher's inbox with Cloudflare Email Routing. Forwarding only receives mail, so it doesn't change how SES sends.
|
|
276
|
+
|
|
277
|
+
1. Check that your From address has no inbox yet. This prints nothing:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
dig +short MX send.example.com
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
If it lists mail servers, `send.example.com` already receives mail elsewhere, and Email Routing would replace those servers. Check that your mail host has a mailbox for `newsletter@send.example.com`, and skip the rest of this section.
|
|
284
|
+
|
|
285
|
+
1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
|
|
286
|
+
|
|
287
|
+
1. Add `send.example.com` to Email Routing: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
|
|
288
|
+
|
|
289
|
+
1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
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.
|
|
294
|
+
|
|
295
|
+
1. **(Optional)** Give your DMARC reports an inbox too. If the address in your DMARC record doesn't receive mail, add a rule that forwards `dmarc` the same way. Then change the record's `rua` to `mailto:dmarc@send.example.com`.
|
|
296
|
+
|
|
297
|
+
1. Check that your From address receives mail. This now lists Cloudflare's three `route` mail servers:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
dig +short MX send.example.com
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Out-of-office and other automatic replies reach the publisher's inbox too. Bounces don't: they go to SES, which reports them to the app. The publisher answers from their own inbox, not from `newsletter@send.example.com`. To answer from the newsletter's address instead, give it a mailbox at a mail host such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034), in place of forwarding.
|
|
304
|
+
|
|
305
|
+
SES is now connected in the sandbox. Until AWS grants production access, it sends only to the addresses you verified in section 3. It also sends to the SES test addresses Verify it works uses. SES charges for each email it sends, test emails included, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
|
|
306
|
+
|
|
307
|
+
Next, [request production access](06-production-access.md).
|
|
@@ -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).
|