@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/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 Resend](get-started/04-resend.md)
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. You need what the [Overview](01-overview.md#before-you-begin) lists.
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 on your computer before you deploy it. Local development sends email to a stand-in, so nothing reaches a real inbox:
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
- - **`PROVIDER`** stays `fake` until you connect Resend in [step 4](04-resend.md). The `fake` transport delivers nothing, so don't schedule a post before then.
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 every request instead answers `500` with a body like `{"error":"invalid_config","variable":"APP_ORIGIN",…}`, the setting it names is missing or malformed. Fix it in `wrangler.jsonc`, commit, and deploy again.
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. With either one missing, it can't verify any login, so it lets no one in.
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. One may be missing or mistyped, or `ACCESS_ALLOWED_EMAILS` may leave out your address.
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 Resend](04-resend.md).
108
+ Next, [connect an email provider](04-resend.md).
@@ -1,6 +1,6 @@
1
- # Connect Resend
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. These guides use Resend. To use Amazon SES instead, follow [Use Amazon SES instead of Resend](../guides/02-ses.md) in place of this page.
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. Switch the provider and deploy
81
+ ## 5. Set the provider and deploy
82
82
 
83
- Store both secrets before you switch. With `PROVIDER` set to `resend` and a secret missing, every request answers `500` and names the missing secret.
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`, change `PROVIDER` from `fake` to `resend`:
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 Resend](04-resend.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
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 Resend](04-resend.md#4-add-the-webhook).
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
- Your instance works. What's left is making it yours, and telling people where to subscribe.
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, and mails no one twice. For a large list, [Amazon SES](../guides/02-ses.md) may cost less.
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
@@ -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 Resend](../get-started/04-resend.md).
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`, change these values:
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: `fake`, `resend`, or `ses` | required |
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
- `fake` delivers nothing. It records a send as if every recipient accepted it. Local development runs on it, and so does a new deployment until its provider is connected. Never schedule a real post on it.
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` must be exactly `fake`, `resend`, or `ses`. A misspelling, or `SES`, is refused, never taken for `fake`.
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
- - With a real provider, none of `APP_ORIGIN`, `ARCHIVE_ORIGIN`, `MEDIA_PUBLIC_BASE`, `SENDING_DOMAIN`, or `FROM_ADDRESS` may still be on `example.com`, the template's placeholder.
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
- Until it's fixed, every request answers `500` with a body naming the variable, such as `{"error":"invalid_config","variable":"APP_ORIGIN",…}`. The Worker's log records it, and no sends go out. The editor's page still loads, since it's a static file, but everything it asks the API for fails the same way.
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 Resend](../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.
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.6.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
- "description": "Newsletter app: write in Markdown, preview, schedule with a review window, and send to a double opt-in list. Cloudflare Worker + D1 + R2 + SES/Resend.",
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"
@@ -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; a real provider
19
- // refuses to start while an origin or FROM_ADDRESS is still on example.com.
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 starts on `fake`: the setup guide's provider step switches it once the
75
- // provider's secrets are set.
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).