@kurtbruns/kestrel 1.6.0 → 1.7.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 +5 -17
- package/CHANGELOG.md +26 -1
- package/README.md +1 -1
- package/dist/public/dashboard/app.js +196 -193
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +40 -0
- package/dist/worker/index.js +210 -69
- package/docs/README.md +3 -3
- package/docs/get-started/02-deploy.md +29 -13
- package/docs/get-started/03-access.md +3 -3
- package/docs/get-started/04-resend.md +8 -6
- package/docs/get-started/05-verify.md +2 -2
- package/docs/get-started/06-go-live.md +2 -2
- package/docs/guides/02-ses.md +2 -2
- package/docs/guides/08-try-locally.md +108 -0
- package/docs/reference/01-configuration.md +17 -6
- package/docs/reference/02-sending-domain-dns.md +1 -1
- package/package.json +2 -2
- package/template/wrangler.jsonc +7 -6
- package/docs/get-started/01-overview.md +0 -47
package/docs/README.md
CHANGED
|
@@ -4,12 +4,11 @@ These guides deploy and run your own instance of Kestrel, from an empty reposito
|
|
|
4
4
|
|
|
5
5
|
## Get started
|
|
6
6
|
|
|
7
|
-
Follow these steps, from an empty repository to a live newsletter.
|
|
7
|
+
Follow these steps, from an empty repository to a live newsletter. Each one ends with a check, so you know it worked before you move on.
|
|
8
8
|
|
|
9
|
-
1. [Overview](get-started/01-overview.md)
|
|
10
9
|
1. [Deploy the app](get-started/02-deploy.md)
|
|
11
10
|
1. [Lock the dashboard with Access](get-started/03-access.md)
|
|
12
|
-
1. [Connect
|
|
11
|
+
1. [Connect an email provider](get-started/04-resend.md)
|
|
13
12
|
1. [Verify it works](get-started/05-verify.md)
|
|
14
13
|
1. [Go live](get-started/06-go-live.md)
|
|
15
14
|
|
|
@@ -18,6 +17,7 @@ Follow these steps, from an empty repository to a live newsletter.
|
|
|
18
17
|
Each one stands on its own. Pick the ones you need.
|
|
19
18
|
|
|
20
19
|
- [Connect Claude to the API](guides/01-connect-claude.md)
|
|
20
|
+
- [Try Kestrel locally first](guides/08-try-locally.md)
|
|
21
21
|
- [Use Amazon SES instead of Resend](guides/02-ses.md)
|
|
22
22
|
- [Notifications through Cloudflare's email](guides/03-notifications.md)
|
|
23
23
|
- [Rate-limit the subscribe form](guides/05-rate-limit.md)
|
|
@@ -2,7 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Kestrel runs as one Cloudflare Worker, with a D1 database for posts and subscribers and an R2 bucket for images.
|
|
4
4
|
|
|
5
|
-
In this guide, you create your instance's repository, create the database and the bucket, and point Kestrel's production settings at them and at your hostname. Then you deploy, and check that the app answers on your hostname.
|
|
5
|
+
In this guide, you choose your hostnames, create your instance's repository, create the database and the bucket, and point Kestrel's production settings at them and at your hostname. Then you deploy, and check that the app answers on your hostname.
|
|
6
|
+
|
|
7
|
+
## Before you begin
|
|
8
|
+
|
|
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
|
+
- [Node.js](https://nodejs.org/) 22 or later, and [Git](https://git-scm.com/).
|
|
11
|
+
- A GitHub account, or another Git host, for your instance's repository.
|
|
12
|
+
- A provider for sending email. [Resend](https://resend.com/signup) is a good default, and the one these guides walk through.
|
|
13
|
+
|
|
14
|
+
## Choose your hostnames
|
|
15
|
+
|
|
16
|
+
Kestrel uses two hostnames: one for the app, and one your email is sent from.
|
|
17
|
+
|
|
18
|
+
| Hostname | What it's for |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `newsletter.example.com` | the editor, the API, and the public pages readers see |
|
|
21
|
+
| `send.example.com` | the address your email comes from, and its SPF, DKIM, and DMARC records |
|
|
22
|
+
|
|
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
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
Throughout these guides, replace the example names with your own, and any value written as `REPLACE_WITH_…`.
|
|
6
28
|
|
|
7
29
|
## 1. Create your instance's repository
|
|
8
30
|
|
|
@@ -34,14 +56,7 @@ Your instance's repository holds its configuration, its database migrations, and
|
|
|
34
56
|
git push -u origin HEAD
|
|
35
57
|
```
|
|
36
58
|
|
|
37
|
-
1. **(Optional)** Try Kestrel
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
cp .dev.vars.example .dev.vars
|
|
41
|
-
npm run dev
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Open http://localhost:8787/dashboard/ to see the editor. To fill it with demo content, run `npm run seed` in a second terminal in the same folder, and `npm run reset` to empty it again. Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to stop the server.
|
|
59
|
+
1. **(Optional)** Try Kestrel locally before you deploy it. Your instance is written and installed, so start at [Start the app](../guides/08-try-locally.md#2-start-the-app), and come back here when you're done. Email goes to a stand-in, so nothing reaches a real inbox.
|
|
45
60
|
|
|
46
61
|
## 2. Sign in to Cloudflare
|
|
47
62
|
|
|
@@ -95,7 +110,6 @@ Every command from here on acts on your Cloudflare account through the Wrangler
|
|
|
95
110
|
}
|
|
96
111
|
],
|
|
97
112
|
"vars": {
|
|
98
|
-
"PROVIDER": "fake", // ← leave as is for now
|
|
99
113
|
"APP_ORIGIN": "https://newsletter.example.com", // ← your app's hostname
|
|
100
114
|
"ARCHIVE_BASE_PATH": "/archive",
|
|
101
115
|
"SENDING_DOMAIN": "send.example.com", // ← your sending hostname
|
|
@@ -107,10 +121,12 @@ Every command from here on acts on your Cloudflare account through the Wrangler
|
|
|
107
121
|
What each one does:
|
|
108
122
|
|
|
109
123
|
- **`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.
|
|
110
|
-
-
|
|
124
|
+
- **No `PROVIDER` yet.** You add it in step 3, [Connect an email provider](04-resend.md). Until then the app runs without an email provider: you can write and preview posts, but scheduling, test emails, and signups are refused.
|
|
111
125
|
- **`ARCHIVE_BASE_PATH`** is permanent once you send. Every post you send carries its `/archive/…` link, and changing the prefix later breaks the links you've already mailed. Keep `/archive` unless you have a reason not to.
|
|
112
126
|
- **`FROM_ADDRESS`** is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`.
|
|
113
127
|
|
|
128
|
+
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.
|
|
129
|
+
|
|
114
130
|
Every other setting has a working default. [Configuration](../reference/01-configuration.md) lists them all, and what the app refuses.
|
|
115
131
|
|
|
116
132
|
1. Check the file. This regenerates the binding types, and fails if the configuration doesn't parse:
|
|
@@ -166,8 +182,8 @@ This deploys the release your instance runs, with your settings. The output ends
|
|
|
166
182
|
401
|
|
167
183
|
```
|
|
168
184
|
|
|
169
|
-
1. In a browser, `https://newsletter.example.com/` shows your newsletter's public landing page.
|
|
185
|
+
1. In a browser, `https://newsletter.example.com/` shows your newsletter's public landing page. Its subscribe page, `/subscribe`, says signups aren't open yet, until step 3, Connect an email provider.
|
|
170
186
|
|
|
171
|
-
If
|
|
187
|
+
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.
|
|
172
188
|
|
|
173
189
|
Next, [lock the dashboard with Access](03-access.md).
|
|
@@ -71,7 +71,7 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
71
71
|
npx wrangler secret put ACCESS_AUD --env production
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
A secret takes effect as soon as you store it, with no redeploy. The app needs both values.
|
|
74
|
+
A secret takes effect as soon as you store it, with no redeploy. The app needs both values. While only one is stored, it refuses its configuration and names the missing one: the editor says so, `/health` answers `500` with `"variable":"ACCESS_AUD"`, and the public pages say the newsletter is temporarily unavailable. Storing the second clears it.
|
|
75
75
|
|
|
76
76
|
1. **(Optional, recommended)** Limit the editor to named people. The Access policy decides who can sign in. This list keeps that true if the policy is widened later, for another app or by mistake. Enter a comma-separated list of email addresses at the prompt:
|
|
77
77
|
|
|
@@ -103,6 +103,6 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
103
103
|
{"status":"ok","service":"kestrel"}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
If Cloudflare lets you in but the editor answers `401`, check the two secrets
|
|
106
|
+
If the editor says the configuration needs a fix, store the secret it names. If Cloudflare lets you in but the editor answers `401`, check the two secrets for a typo, or `ACCESS_ALLOWED_EMAILS` may leave out your address.
|
|
107
107
|
|
|
108
|
-
Next, [connect
|
|
108
|
+
Next, [connect an email provider](04-resend.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Connect
|
|
1
|
+
# Connect an email provider
|
|
2
2
|
|
|
3
|
-
Kestrel sends your posts through an email provider. The provider only delivers the mail: Kestrel keeps your list, each subscriber's consent, and the record of every send.
|
|
3
|
+
Kestrel sends your posts through an email provider. The provider only delivers the mail: Kestrel keeps your list, each subscriber's consent, and the record of every send. This page connects Resend. To use Amazon SES instead, follow [Use Amazon SES instead of Resend](../guides/02-ses.md) in place of this page.
|
|
4
4
|
|
|
5
5
|
In this guide, you verify your sending hostname with Resend and add a DMARC record. Then you give the app an API key and a webhook for bounces and complaints, switch it to Resend, and deploy.
|
|
6
6
|
|
|
@@ -78,11 +78,11 @@ The webhook tells the app when an address bounces or complains, and the app stop
|
|
|
78
78
|
npx wrangler secret put RESEND_WEBHOOK_SECRET --env production
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
## 5.
|
|
81
|
+
## 5. Set the provider and deploy
|
|
82
82
|
|
|
83
|
-
Store both secrets before you
|
|
83
|
+
Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` connects Resend. Store both secrets before you set it. With `PROVIDER` set to `resend` and a secret missing, every request answers `500` and names the missing secret.
|
|
84
84
|
|
|
85
|
-
1. In `wrangler.jsonc`, in the `production` block's `vars`,
|
|
85
|
+
1. In `wrangler.jsonc`, in the `production` block's `vars`, add `PROVIDER` set to `resend`:
|
|
86
86
|
|
|
87
87
|
```jsonc
|
|
88
88
|
"PROVIDER": "resend",
|
|
@@ -125,7 +125,9 @@ Store both secrets before you switch. With `PROVIDER` set to `resend` and a secr
|
|
|
125
125
|
|
|
126
126
|
Any other answer, such as a redirect to your Cloudflare login, means Access covers the webhook. Remove that path from your Access application.
|
|
127
127
|
|
|
128
|
-
1. In the editor, **Settings → Email sender** shows Resend as the email provider, and your From address.
|
|
128
|
+
1. In the editor, **Settings → Email sender** shows Resend as the email provider, and your From address, where it said **No email provider connected** before.
|
|
129
|
+
|
|
130
|
+
1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
|
|
129
131
|
|
|
130
132
|
The test sends in the next step prove the rest.
|
|
131
133
|
|
|
@@ -33,7 +33,7 @@ Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing
|
|
|
33
33
|
|
|
34
34
|
With Amazon SES and no custom MAIL FROM domain, SPF passes for `amazonses.com` instead. That's expected, and DMARC still passes.
|
|
35
35
|
|
|
36
|
-
If a check fails, compare your DNS records with the ones in [Connect
|
|
36
|
+
If a check fails, compare your DNS records with the ones in [Connect an email provider](04-resend.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
|
|
37
37
|
|
|
38
38
|
## 3. Set up your notifications
|
|
39
39
|
|
|
@@ -82,7 +82,7 @@ When an address bounces or marks your mail as spam, your provider tells the app
|
|
|
82
82
|
|
|
83
83
|
1. In the editor, **Subscribers** lists both addresses as **Pending**, each with a **Suppressed** flag. The app never mails a suppressed address again.
|
|
84
84
|
|
|
85
|
-
If an address has no flag, the webhook isn't reaching the app. Check its URL and signing secret in [Connect
|
|
85
|
+
If an address has no flag, the webhook isn't reaching the app. Check its URL and signing secret in [Connect an email provider](04-resend.md#4-add-the-webhook).
|
|
86
86
|
|
|
87
87
|
## 7. Read the logs
|
|
88
88
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Go live
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This last step makes the newsletter yours, and tells people where to subscribe.
|
|
4
4
|
|
|
5
5
|
In this guide, you name your publication and share your subscribe link. You also see what to change as your list grows.
|
|
6
6
|
|
|
@@ -30,7 +30,7 @@ Readers subscribe on a page the app hosts. Every signup confirms by email before
|
|
|
30
30
|
|
|
31
31
|
A new instance runs on free plans. As your list grows, two limits come into play.
|
|
32
32
|
|
|
33
|
-
- **Your email provider's limits.** Resend's free plan sends up to 100 emails a day. A send that hits the limit waits, and the app emails you. It resumes on its own once the limit lifts, or after you upgrade your Resend plan
|
|
33
|
+
- **Your email provider's limits.** Resend's free plan sends up to 100 emails a day. A send that hits the limit waits, and the app emails you. It resumes on its own once the limit lifts, or after you upgrade your Resend plan. For a large list, [Amazon SES](../guides/02-ses.md) may cost less.
|
|
34
34
|
- **How fast a send goes out.** On Cloudflare's free Workers plan, Resend sends about 400 recipients a minute. On the $5 Workers Paid plan, set [`SUBREQUEST_BUDGET`](../reference/01-configuration.md#subrequest_budget) to send far more each minute.
|
|
35
35
|
|
|
36
36
|
## Check it
|
package/docs/guides/02-ses.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Amazon SES costs less than Resend for a large list, but takes more setup. AWS reviews your account before it lets you send to anyone but yourself. Kestrel also needs Cloudflare's Workers Paid plan to send through SES at a useful pace.
|
|
4
4
|
|
|
5
|
-
In this guide, you verify your sending hostname with SES, ask AWS for production access, and route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, switch it to SES, and deploy. Follow it in place of [Connect
|
|
5
|
+
In this guide, you verify your sending hostname with SES, ask AWS for production access, and route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, switch it to SES, and deploy. Follow it in place of [Connect an email provider](../get-started/04-resend.md).
|
|
6
6
|
|
|
7
7
|
## Before you begin
|
|
8
8
|
|
|
@@ -177,7 +177,7 @@ Store every secret before you switch. With `PROVIDER` set to `ses` and a secret
|
|
|
177
177
|
npx wrangler secret put SES_CONFIGURATION_SET --env production
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
1. In `wrangler.jsonc`, in the `production` block's `vars`,
|
|
180
|
+
1. In `wrangler.jsonc`, in the `production` block's `vars`, set these values, adding `PROVIDER` if it isn't there yet:
|
|
181
181
|
|
|
182
182
|
```jsonc
|
|
183
183
|
"PROVIDER": "ses",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Try Kestrel locally first
|
|
2
|
+
|
|
3
|
+
You can run Kestrel on your own computer to look around before you deploy it. It needs no Cloudflare account and no Git host. Email goes to a stand-in on your computer, so nothing reaches a real inbox.
|
|
4
|
+
|
|
5
|
+
In this guide, you write a new instance into an empty folder, start the app, and open the editor. Then you fill it with a demo publication, and empty it again. The folder can become your instance's repository later.
|
|
6
|
+
|
|
7
|
+
## Before you begin
|
|
8
|
+
|
|
9
|
+
- [Node.js](https://nodejs.org/) 22 or later
|
|
10
|
+
|
|
11
|
+
## 1. Write a new instance
|
|
12
|
+
|
|
13
|
+
An instance is a small folder of your own. It holds the instance's settings and names the release of Kestrel it runs, which installs from npm as `@kurtbruns/kestrel`.
|
|
14
|
+
|
|
15
|
+
1. Make an empty folder, and go into it:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
mkdir newsletter
|
|
19
|
+
cd newsletter
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
1. Write a new instance into it:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @kurtbruns/kestrel init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It writes the instance's files, and lists the commands to run next. The folder doesn't need to be a Git repository.
|
|
29
|
+
|
|
30
|
+
1. Install the release and its tools:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. Start the app
|
|
37
|
+
|
|
38
|
+
1. Copy the local settings:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cp .dev.vars.example .dev.vars
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
They're for your computer only, and send email to the stand-in.
|
|
45
|
+
|
|
46
|
+
1. Start the app:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The first time, it creates the local database and asks you to confirm its migrations. Answer yes. The server keeps running in this terminal, and its output ends with the address it's ready on.
|
|
53
|
+
|
|
54
|
+
1. Open http://localhost:8787/dashboard/ to see the editor. There's nothing to sign in with on your computer. If another app already uses port 8787, Kestrel picks a free port instead, and the `Ready on` line shows it.
|
|
55
|
+
|
|
56
|
+
## 3. Load the demo publication
|
|
57
|
+
|
|
58
|
+
The demo fills the editor with posts, subscribers, and sends that already went out, so you can see the app in use.
|
|
59
|
+
|
|
60
|
+
1. In a second terminal, in the same folder, load the demo:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm run seed
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It replaces everything in the local database, and lists links to the editor and the demo's published posts.
|
|
67
|
+
|
|
68
|
+
1. To empty the local database again, and see the app as a new instance starts:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run reset
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
1. To stop the server, press <kbd>Ctrl</kbd>+<kbd>C</kbd> in its terminal. Your local content stays for the next `npm run dev`.
|
|
75
|
+
|
|
76
|
+
## Check it
|
|
77
|
+
|
|
78
|
+
1. The app answers. If `npm run dev` picked another port, use the one on its `Ready on` line here and below:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl http://localhost:8787/health
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
{"status":"ok","service":"kestrel"}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
1. After `npm run seed`, http://localhost:8787/ shows the demo publication's public landing page, with its posts.
|
|
89
|
+
|
|
90
|
+
## Keep the folder for your instance
|
|
91
|
+
|
|
92
|
+
If you came here from Deploy the app, your folder is already a repository: go back to [Sign in to Cloudflare](../get-started/02-deploy.md#2-sign-in-to-cloudflare).
|
|
93
|
+
|
|
94
|
+
When you're ready to deploy, this folder can be your instance's repository. You need [Git](https://git-scm.com/) and a GitHub account for this part. The folder's `.gitignore` already keeps your local settings, the local database, and the installed packages out of it.
|
|
95
|
+
|
|
96
|
+
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`.
|
|
97
|
+
|
|
98
|
+
1. Make the folder a repository, and commit the instance. Replace `YOUR_USERNAME` with your GitHub username:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git init
|
|
102
|
+
git add -A
|
|
103
|
+
git commit -m "Start a Kestrel instance"
|
|
104
|
+
git remote add origin https://github.com/YOUR_USERNAME/newsletter.git
|
|
105
|
+
git push -u origin HEAD
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then check [what deploying needs](../get-started/02-deploy.md#before-you-begin) and [choose your hostnames](../get-started/02-deploy.md#choose-your-hostnames). Then [deploy the app](../get-started/02-deploy.md#2-sign-in-to-cloudflare) from signing in to Cloudflare onward.
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Every deploy-time setting: what it does, its default, and what the app refuses. Each environment's settings live in its block under `env` in `wrangler.jsonc`, and its secrets in Cloudflare.
|
|
4
4
|
|
|
5
|
-
- **Vars** are public values in the block's `vars`, safe to commit.
|
|
5
|
+
- **Vars** are public values in the block's `vars`, safe to commit. A change takes effect when you deploy.
|
|
6
6
|
- **Secrets** are stored with `npx wrangler secret put NAME --env production`, and never committed. A secret takes effect as soon as you store it, with no redeploy.
|
|
7
7
|
- **Bindings and triggers** connect the Worker to its database, its image bucket, and its schedule.
|
|
8
8
|
|
|
9
|
+
In local development, `npm run dev` reads the top level of `wrangler.jsonc` and `.dev.vars`, which you copy from `.dev.vars.example`. Restart it after you change either.
|
|
10
|
+
|
|
9
11
|
None of these can be read or changed from inside the app, by the editor or by Claude. Settings in the editor shows the public ones, read-only.
|
|
10
12
|
|
|
11
13
|
## Vars
|
|
12
14
|
|
|
13
15
|
| Var | What it is | Default |
|
|
14
16
|
| --- | --- | --- |
|
|
15
|
-
| `PROVIDER` | the email provider: `
|
|
17
|
+
| `PROVIDER` | the email provider: `resend` or `ses`, or `fake` for local development | none: no provider connected |
|
|
16
18
|
| `APP_ORIGIN` | the origin the app is served from: scheme and host, no path | required |
|
|
17
19
|
| `ARCHIVE_BASE_PATH` | the path the archive lives under | `/archive` |
|
|
18
20
|
| `FROM_ADDRESS` | the `From:` of every email | required with a real provider |
|
|
@@ -27,7 +29,9 @@ None of these can be read or changed from inside the app, by the editor or by Cl
|
|
|
27
29
|
|
|
28
30
|
### `PROVIDER`
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
Until it's set, the app runs with no provider connected. You can write and preview posts, but everything that would send email is refused: scheduling, sending now, test emails, and the confirmation emails that signups need. The subscribe page says signups aren't open yet, and Settings shows **No email provider connected**. Setting it to `none` means the same. [Connect an email provider](../get-started/04-resend.md) sets it.
|
|
33
|
+
|
|
34
|
+
`fake` delivers nothing. It records a send as if every recipient accepted it. Local development runs on it, and names it on purpose in the top level of `wrangler.jsonc`. Never set it on a deployment that has readers: every subscriber would be recorded as sent the post, and never get it.
|
|
31
35
|
|
|
32
36
|
### `FROM_ADDRESS` and `SENDING_DOMAIN`
|
|
33
37
|
|
|
@@ -129,10 +133,17 @@ When declared, notifications go through Cloudflare's email instead of your provi
|
|
|
129
133
|
|
|
130
134
|
The app checks its configuration on every request. It refuses to run on one that would do the wrong thing quietly:
|
|
131
135
|
|
|
132
|
-
- `PROVIDER
|
|
136
|
+
- `PROVIDER`, when set, must be exactly `resend`, `ses`, `fake`, or `none`. A misspelling, or `SES`, is refused, never taken for another.
|
|
133
137
|
- `APP_ORIGIN` must be a plain `http` or `https` origin, with no path. So must `ARCHIVE_ORIGIN`, when set. `MEDIA_PUBLIC_BASE`, when set, must be a plain `http` or `https` URL, and may have a path. A trailing slash is dropped from each.
|
|
134
138
|
- A real provider needs `FROM_ADDRESS`, and every secret the table marks for it. SES also needs `AWS_REGION`.
|
|
135
|
-
-
|
|
139
|
+
- None of `APP_ORIGIN`, `ARCHIVE_ORIGIN`, `MEDIA_PUBLIC_BASE`, `SENDING_DOMAIN`, or `FROM_ADDRESS` may still be on `example.com` or a name under it, the template's placeholder. That holds with no provider connected too, since every link the app builds would name it. Only `fake` allows it.
|
|
136
140
|
- `SUBREQUEST_BUDGET`, `MIN_LEAD_SECONDS`, and `SES_MAX_SEND_RATE` must be whole numbers above zero. `MIN_LEAD_SECONDS` must also be within its bounds. Any other value is refused, never adjusted. The one exception is a `SUBREQUEST_BUDGET` below 30, which is raised to 30.
|
|
141
|
+
- In local development, `SIMULATE_SENDS` must be `off`, or the provider a local send models, `resend`, `ses`, or `1` for a generic one, optionally followed by `:none` for no injected failures. A deployed instance ignores it.
|
|
142
|
+
- `ACCESS_TEAM_DOMAIN` and `ACCESS_AUD` are set together or not at all. With only one, the app could verify no sign-in and would let no one into the editor, so it refuses and names the missing one.
|
|
143
|
+
|
|
144
|
+
Until it's fixed, no sends go out, and each place says what its reader can act on:
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
- **The editor** shows a page naming the setting, what's wrong with it, and how to fix it, with a link to its section here. Its own Docs tab can't load until then, so the link opens the project's site.
|
|
147
|
+
- **The API**, `/health`, and the webhooks answer `500` with a body naming the variable, such as `{"error":"invalid_config","variable":"APP_ORIGIN",…}`.
|
|
148
|
+
- **The public pages readers see** answer `503` with a page saying the newsletter is temporarily unavailable. It names no setting.
|
|
149
|
+
- **The Worker's log** records it as `config.invalid`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sending-domain DNS
|
|
2
2
|
|
|
3
|
-
Inboxes decide whether to trust your mail by checking records in the DNS of the domain it comes from. This page explains the records that [Connect
|
|
3
|
+
Inboxes decide whether to trust your mail by checking records in the DNS of the domain it comes from. This page explains the records that [Connect an email provider](../get-started/04-resend.md) and [Use Amazon SES instead of Resend](../guides/02-ses.md) have you add. It covers what each one proves, how to check them, and when to tighten DMARC.
|
|
4
4
|
|
|
5
5
|
## Why all three
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kurtbruns/kestrel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "An open-source, self-hosted newsletter app where you own the list and the archive. Work in the editor, or just ask Claude. Runs on Cloudflare Workers.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22"
|
package/template/wrangler.jsonc
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// The top level is the DEVELOPMENT environment, which `npm run dev` runs. Its mail
|
|
16
16
|
// transport is an in-memory stand-in, so local dev can never reach a real inbox. The
|
|
17
17
|
// `production` environment below must REDECLARE every binding and var, because wrangler
|
|
18
|
-
// does not inherit them. Fill in its values as the setup guide says;
|
|
19
|
-
//
|
|
18
|
+
// does not inherit them. Fill in its values as the setup guide says; the app refuses to
|
|
19
|
+
// start while an origin or a sending setting is still on example.com, provider or not.
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
22
22
|
// The send sweep, once a minute. `npm run dev` runs it locally, where wrangler never
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
|
|
43
43
|
// ARCHIVE_ORIGIN and MEDIA_PUBLIC_BASE default to the app's own origin; set them only to
|
|
44
44
|
// put the archive on a website's apex or serve images from their own domain (the guide's
|
|
45
|
-
// configuration reference says what each needs).
|
|
45
|
+
// configuration reference says what each needs). PROVIDER is `fake` here, a stand-in that
|
|
46
|
+
// delivers nothing, so local development never reaches a real inbox.
|
|
46
47
|
"vars": {
|
|
47
48
|
"PROVIDER": "fake",
|
|
48
49
|
"APP_ORIGIN": "http://localhost:8787",
|
|
@@ -71,10 +72,10 @@
|
|
|
71
72
|
}
|
|
72
73
|
],
|
|
73
74
|
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "kestrel-media-production" }],
|
|
74
|
-
// PROVIDER
|
|
75
|
-
//
|
|
75
|
+
// No PROVIDER yet: the app runs but sends nothing until the setup guide's provider
|
|
76
|
+
// step sets one, once its secrets are stored. Never `fake` here, which records every
|
|
77
|
+
// send as delivered and mails no one.
|
|
76
78
|
"vars": {
|
|
77
|
-
"PROVIDER": "fake",
|
|
78
79
|
"APP_ORIGIN": "https://newsletter.example.com",
|
|
79
80
|
"ARCHIVE_BASE_PATH": "/archive",
|
|
80
81
|
"SENDING_DOMAIN": "send.example.com",
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Overview
|
|
2
|
-
|
|
3
|
-
Kestrel is a newsletter app you run yourself. You write a post in Markdown, preview it exactly as the email, and send it to a list you own. It runs on Cloudflare Workers, and an email provider delivers the mail.
|
|
4
|
-
|
|
5
|
-
In this guide, you deploy your own instance of Kestrel to your Cloudflare account and connect it to an email provider. You go from an empty repository to a newsletter that's ready for its first subscriber. Each step ends with a check, so you know it worked before you move on.
|
|
6
|
-
|
|
7
|
-
You run every step from your instance's repository, a small repository of your own. It holds your instance's configuration and names the release of Kestrel it runs, which installs from npm as `@kurtbruns/kestrel`. You return to it to deploy changes and upgrade to new releases. You don't need to run Kestrel on your computer to deploy it, but you can try it there first, with demo content.
|
|
8
|
-
|
|
9
|
-
## Before you begin
|
|
10
|
-
|
|
11
|
-
You need:
|
|
12
|
-
|
|
13
|
-
1. 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.
|
|
14
|
-
|
|
15
|
-
1. An account with an email provider: [Resend](https://resend.com/signup) or [Amazon SES](https://aws.amazon.com/ses/). These guides use Resend, the quicker to set up. Amazon SES costs less for a large list. It needs AWS to approve your account before it sends to subscribers, and Cloudflare's Workers Paid plan. To use SES, follow [Use Amazon SES instead of Resend](../guides/02-ses.md) in place of step 4.
|
|
16
|
-
|
|
17
|
-
1. [Node.js](https://nodejs.org/) 22 or later, and [Git](https://git-scm.com/).
|
|
18
|
-
|
|
19
|
-
1. A GitHub account, or another Git host, for your instance's repository.
|
|
20
|
-
|
|
21
|
-
## Choose your hostnames
|
|
22
|
-
|
|
23
|
-
Kestrel uses two hostnames, because the app and the mail do different jobs:
|
|
24
|
-
|
|
25
|
-
| Hostname | What it's for |
|
|
26
|
-
| --- | --- |
|
|
27
|
-
| `newsletter.example.com` | the editor, the API, and the public pages readers see |
|
|
28
|
-
| `send.example.com` | the address your mail comes from, and its SPF, DKIM, and DMARC records |
|
|
29
|
-
|
|
30
|
-
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`.
|
|
31
|
-
|
|
32
|
-
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.
|
|
33
|
-
|
|
34
|
-
Throughout these guides, replace `example.com`, `newsletter.example.com`, and `send.example.com` with your own names. Replace any value written as `REPLACE_WITH_…` too.
|
|
35
|
-
|
|
36
|
-
## The steps
|
|
37
|
-
|
|
38
|
-
1. **Overview**: what you need, and the hostnames you use.
|
|
39
|
-
1. [Deploy the app](02-deploy.md): create your instance's repository, and put the app on your hostname.
|
|
40
|
-
1. [Lock the dashboard with Access](03-access.md): add the login that keeps the editor and the API to you.
|
|
41
|
-
1. [Connect Resend](04-resend.md): verify your sending domain, and connect Resend to the app.
|
|
42
|
-
1. [Verify it works](05-verify.md): send real email to yourself, and test unsubscribing and bounces.
|
|
43
|
-
1. [Go live](06-go-live.md): name your publication and share the subscribe link.
|
|
44
|
-
|
|
45
|
-
Further guides, like [connecting Claude](../guides/01-connect-claude.md) and [upgrading to a new release](../guides/07-upgrade.md), pick up from there.
|
|
46
|
-
|
|
47
|
-
When you're ready, continue to [Deploy the app](02-deploy.md).
|