@kurtbruns/kestrel 1.15.1 → 1.17.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.
@@ -1,108 +1,69 @@
1
1
  # Troubleshooting
2
2
 
3
- When a send needs you, its page and the dashboard's health block say so in a red or amber line. Red needs you to act, and amber is worth a look. Find the line's words below for what it means and what to do.
3
+ When something needs you, Kestrel says so in a line on the send's page, under **Sent**. The top of the dashboard shows it too. Red needs you to act, and amber is worth a look. The line says what happened, what to do, and who does it. This page covers what the screen can't: the fixes made outside Kestrel, and the decisions that are yours.
4
4
 
5
5
  **Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**. With none set, Kestrel emails you nothing. [Notifications](06-troubleshooting.md#notifications), below, lists what you're told.
6
6
 
7
- ## A send is waiting or held
7
+ ## A send is held or waiting
8
8
 
9
- A problem with your email provider never marks anyone unsent. The send waits where it is, and carries on from there once the problem clears. "No one has been marked unsent because of this" means this problem: readers whose own email failed earlier stay unsent.
9
+ Nothing is lost. A problem with your email provider never marks anyone unsent or mails anyone twice. The send waits where it is, and carries on once the problem is fixed. The send's line names the fix. When that fix is outside Kestrel, whoever runs your instance makes it, and it's one of these:
10
10
 
11
- - **"The provider is refusing the account".** Your provider won't take mail from your account, and its own words follow, with the fix and who makes it. The fix is almost always in your Amazon SES account or your instance, made by whoever runs it. [What SES refuses, and the fix](#what-ses-refuses-and-the-fix), below, lists each one. Any other post that comes due meanwhile waits with it, without asking SES, and you're told once. While a quota frees up a few at a time, or while the SES sandbox takes only your verified addresses, SES takes some readers on each retry and refuses the rest: those it takes are sent, and the line stays, with the time it began, until SES takes everyone it's asked to.
12
- - **"The provider is refusing this send".** SES refuses this post's email itself, or a batch of it as invalid, rather than your account. Its words follow. Other posts go out as usual. If SES's words name a reader's address, remove that subscriber: the send skips them and carries on at its next retry. If SES refuses the email itself, the email can't change once its send has started, so whoever runs your instance asks AWS Support why, quoting SES's words.
13
- - **"No email provider is connected".** The send is held until one is connected ([Connect Amazon SES](../get-started/04-ses.md)), and goes out once it is.
14
- - **"The provider is unavailable".** An outage or a rate limit on the provider's side. The send reads **Backing off** and says when it next retries, spacing its retries out up to an hour apart while the outage lasts, so there's nothing to do. Any other post that comes due meanwhile waits with it.
15
- - **"The provider keeps refusing for its sending rate".** An amber line. SES has refused the first request of several retries in a row for its sending rate, which a short burst never does, so Kestrel is set to send faster than your SES account allows. Whoever runs your instance sets `SES_MAX_SEND_RATE` to the **Maximum send rate** on the SES console's **Account dashboard** (1 a second in the sandbox), and deploys, as [Connect Amazon SES](../get-started/04-ses.md#8-set-the-provider-and-deploy) describes. The send retries on its own meanwhile.
16
- - **"Nothing has gone out for …".** A send has handed off no one for half an hour, with no retry or provider problem to explain it. The part of Kestrel that sends has stopped or keeps failing on this send, or another send going out at the same time is taking all of its turn. A large send going out slowly never shows this. If another send is going out, this one picks up once it finishes. Read the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes, and check the Worker's trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes. The send carries on from where it stopped once it runs again, mailing no one twice.
17
- - **"The fire time passed … and the send has not started".** The Cloudflare Cron Trigger that starts sends on time isn't running. The send goes out as soon as it runs again. Check the Worker's trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes.
18
- - **"The provider never answered for …".** Rarely, Kestrel can't tell whether your provider took some emails, and sending again could mail those readers twice. **Resolve…** on the send's page asks you to decide. **Assume not sent** is the safe default: those readers may miss this post, but get your next one. Choose **Assume sent** only when you know they got it.
11
+ - **Amazon SES refuses your account or the send.** The line carries SES's words and the fix, almost always in the SES console. [Connect Amazon SES](../get-started/04-ses.md) sets up each piece SES checks: production access, the sending domain, the AWS key, and the configuration set. When SES refuses your account, any other post that comes due meanwhile waits with it. When it refuses only this send's email, other posts go out as usual. While SES takes some readers and refuses the rest, as while a daily quota frees up, those it takes are sent. The line stays until SES takes everyone.
12
+ - **SES keeps refusing for its sending rate.** Kestrel is set to send faster than your SES account allows. Set `SES_MAX_SEND_RATE` to the maximum send rate on the SES console's account dashboard, which is 1 a second in the sandbox. Then deploy, as [Connect Amazon SES](../get-started/04-ses.md#7-set-the-provider-and-deploy) describes.
13
+ - **No email provider is connected.** Connect one, as [Connect Amazon SES](../get-started/04-ses.md) describes, and the send goes out at its next retry.
14
+ - **A send missed its time, or nothing has gone out for a while.** The part of Kestrel that sends has stopped, or keeps failing on this send. Check the Worker's Cron Trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes, and read the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes. If another send is going out at the same time, this one may only be waiting its turn.
19
15
 
20
- ## Before a send
16
+ ## Someone was mailed twice, or not at all
21
17
 
22
- - **A test says "The provider refused it".** Your provider didn't take the test, and its own words follow, with the fix where Kestrel knows it. Sending again gets the same answer until that's fixed. While Amazon SES is in its sandbox, it sends only to addresses verified in SES, so verify the address you test with in the SES console, or request production access, as [Connect Amazon SES](../get-started/04-ses.md#5-request-production-access) describes. Words about the account, such as a key or the sending domain, point at the same fix as [a send the provider refuses](06-troubleshooting.md#a-send-is-waiting-or-held).
23
- - **A test says "The provider didn't answer, so the test may have arrived".** Kestrel can't tell whether your provider took it. Check the inbox before you send again, since a second test may arrive twice.
24
- - **"The template or publication details changed this email's look after your last test".** Send a new test of the post, since your last one no longer shows what goes out.
25
- - **The daily database writes warning.** On Cloudflare's free Workers plan, a send to more than about 5,000 recipients may use up the day's writes, and then the app stops working until midnight UTC. [Bring your existing list](01-your-list.md#limits) explains the limit, and when to upgrade to Workers Paid.
26
- - **"Workers Free suits a list of up to about 1,000 subscribers".** Your list is past what the free plan suits. Upgrade to Workers Paid ([As your list grows](../guides/09-as-your-list-grows.md)). If the account is on Workers Paid already, set `SUBREQUEST_BUDGET` so Kestrel knows, and the notice goes.
18
+ Kestrel never mails a reader twice on its own. When it can't tell whether your provider took some emails, it stops and asks you: the send's page shows **Resolve…**. Choose **Assume not sent** unless you know otherwise, since it's the safe default: those readers may miss this post, but get your next one. Choose **Assume sent** only when you know they got it, such as from a reader who tells you so. Either choice mails no one.
27
19
 
28
- ## Reading a send's record
20
+ A reader recorded as **Unsent** is still on your list and gets your next post. That send never mails them again: their own email kept failing, SES won't take their address, or you chose **Assume not sent**.
29
21
 
30
- A send's page becomes its permanent record once it finishes. Its counts cover everyone the send went to:
31
-
32
- | Count | What it means |
33
- | --- | --- |
34
- | **Delivered** | Your provider confirmed the email reached the reader's mail server. |
35
- | **Bounced** | The receiving server turned the email away. |
36
- | **Complained** | The reader marked the email as spam. |
37
- | **Unsent** | Kestrel couldn't hand the email to your provider. |
38
-
39
- Delivered always means confirmed, not just accepted by your provider. Confirmations can lag by days, so counts keep rising after a send finishes. Some emails are never confirmed at all, so a send can read **Settling** for good, which is normal, as long as some receipts have come.
22
+ ## A reader says they never got a post
40
23
 
41
- **"No delivery receipt has arrived for any of …".** An hour after a send went out, Kestrel has heard back about none of its readers. Your provider's reports probably aren't reaching Kestrel, so its bounces suppress no one and its complaints unsubscribe no one, and your next post mails those addresses again. Look for `webhook.refused` errors in the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes. It names the SNS topic that reached Kestrel and the one it expects. Then check the subscription in [Connect Amazon SES](../get-started/04-ses.md#9-subscribe-the-app-to-the-topic). The line goes as soon as a receipt arrives.
24
+ Under **Subscribers**, search for their address and open their row. Their page lists every send they were in, newest first, with what happened on each: **Delivered**, a bounce, a complaint, **Unsent** with your email provider's words, or **Skipped** because they had unsubscribed or were suppressed by then. Each send links to its record. The page also shows how they joined and why they left, if they did. Any bounce or block on the address shows with the provider's words or yours. If they aren't on the list, or are still **pending**, they never confirmed, and only confirmed subscribers are sent your posts.
42
25
 
43
- **Unsent recipients** are still on your list, and get your next post. A recipient is unsent when their own email kept failing, or their address isn't one SES accepts, with the provider's words on the row, or when you chose **Assume not sent**.
26
+ ## A send still reads Settling
44
27
 
45
- ### What SES refuses, and the fix
28
+ That's normal. **Delivered** counts only the emails your provider confirmed reached the reader's mail server. Confirmations can lag by days, so the counts keep rising after a send finishes. Some are never confirmed at all, so a send can read **Settling** for good, as long as some receipts have come.
46
29
 
47
- When SES refuses your account or a send, the red line carries SES's words and the fix. Almost every fix is made by whoever runs your instance, in the SES console or its settings.
30
+ When none come, the send's page says so an hour after it went out. Your provider's reports aren't reaching Kestrel, so its bounces suppress no one and its complaints unsubscribe no one. Your next post mails those addresses again. Look for `webhook.refused` errors in the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes. Each names the SNS topic that reached Kestrel and the one it expects. Then check the subscription, as [Connect Amazon SES](../get-started/04-ses.md#8-subscribe-the-app-to-the-topic) describes.
48
31
 
49
- | SES says | What it means | The fix |
50
- | --- | --- | --- |
51
- | Email address is not verified, naming a reader | The SES account is still in the sandbox, which mails only verified addresses. | Request production access on the SES console's **Account dashboard**, as [Connect Amazon SES](../get-started/04-ses.md#5-request-production-access) describes. Signups fail until then, and you're emailed about it once a day. |
52
- | Email address is not verified, naming your From address or its domain | The sending identity isn't verified in this SES region. | Verify the From address or its domain in SES, in the region your instance sends from. |
53
- | Mail FROM domain is not verified | SES can't read the custom MAIL FROM domain's MX record. | Check that record in DNS, or set the identity's **Behavior on MX failure** to use the default MAIL FROM domain. |
54
- | Daily message quota exceeded | The 24-hour sending quota is spent. It frees up on its own. | In the sandbox (200 a day), request production access. Otherwise, ask AWS for a higher quota, as [As your list grows](../guides/09-as-your-list-grows.md) describes. |
55
- | Sending paused (account) | AWS paused the account under review, or sending was turned off by hand. | Follow AWS's email to the AWS account's address, or turn sending back on. |
56
- | Sending paused (configuration set) | Sending is turned off on the configuration set, by hand or by an alarm. | Turn sending back on for that set. |
57
- | Account suspended | AWS shut down the account's sending. | Only AWS can lift it: open a case with AWS Support. |
58
- | An access key, signature, or permission error | Your instance's SES key is wrong, expired, or lacks permission. | Replace the key, or give it `ses:SendEmail` and `ses:SendBulkEmail`. |
59
- | A subscription is needed (OptInRequired) | The AWS account isn't signed up for SES. | Enable SES for the account, or settle its activation or billing with AWS. |
60
- | Configuration set does not exist | `SES_CONFIGURATION_SET` names a set that isn't in this region. | Fix the name, or create the set. |
61
- | Sending pool not found | The configuration set uses a dedicated IP pool that doesn't exist. | Pick an existing pool in the set's sending options, or remove it. |
62
- | The message was rejected | SES refuses this email itself. | Ask AWS Support why. Other posts go out as usual. |
63
- | The request is invalid | SES refused a batch of this send. | If SES's words name a reader's address, remove that subscriber. |
32
+ ## A test doesn't arrive
64
33
 
65
- A server error from SES may come after SES already took the emails, so Kestrel never sends that batch again: its readers wait for **Resolve…**, as for "The provider never answered for …".
34
+ While Amazon SES is in its sandbox, it mails only addresses verified in SES. Verify the address you test with in the SES console, or request production access, as [Connect Amazon SES](../get-started/04-ses.md#4-request-production-access) describes. When the test's line says the provider didn't answer, check the inbox before you send it again, since the first may have arrived. A test SES refuses for your account has the same fix as [a send it refuses](06-troubleshooting.md#a-send-is-held-or-waiting).
66
35
 
67
- ## A reader says they never got a post
36
+ ## Your list outgrows Workers Free
68
37
 
69
- Under **Subscribers**, search for their address and open their row. Their page lists every send they were in, newest first, with what happened on each: **Delivered**, a bounce, a complaint, **Unsent** with your email provider's words, or **Skipped** because they had unsubscribed or were suppressed by then. Each send links to its record. The page also shows how they joined, any bounce or block on the address with the provider's words or yours, and why they left, if they did. If they aren't on the list, or are still **pending**, they never confirmed, and only confirmed subscribers are sent your posts.
38
+ Upgrade the Cloudflare account to Workers Paid, and set `SUBREQUEST_BUDGET` so Kestrel sends faster, as [As your list grows](../guides/09-as-your-list-grows.md) describes. Kestrel warns you before a send that could use up the free plan's daily database writes, which stops the app until midnight UTC. [Bring your existing list](01-your-list.md#limits) explains the limits.
70
39
 
71
40
  ## Bounces and complaints
72
41
 
73
42
  Kestrel acts on these for you, so a bad address can't keep hurting your sending reputation:
74
43
 
75
- - **A hard bounce** means the address can't receive mail. Kestrel never mails it again.
76
- - **A soft bounce** is temporary, such as a full mailbox. The address stays on your list.
77
- - **A complaint** means the reader marked your email as spam. Kestrel unsubscribes them, whichever email they reported, and their page, opened from their row, says they left by a complaint.
44
+ - A hard bounce means the address can't receive mail. Kestrel never mails it again.
45
+ - A soft bounce is temporary, such as a full mailbox. The address stays on your list.
46
+ - A complaint means the reader marked your email as spam. Kestrel unsubscribes them, whichever email they reported, and their page, opened from their row, says they left by a complaint.
78
47
 
79
- Under **Subscribers**, a bounced address keeps its status and carries a label saying why, and its page, opened from its row, gives your email provider's words for the bounce and when it happened. When a recent send's bounces reach about 5% of its recipients, the dashboard warns you. That's about the rate at which email providers start to review a sender, so check the addresses on its record.
48
+ Under **Subscribers**, a bounced address keeps its status and carries a label saying why. Its page gives your email provider's words for the bounce and when it happened. When a recent send's bounces reach about 5% of its recipients, the dashboard warns you. That's about the rate at which email providers start to review a sender, so check the addresses on its record.
80
49
 
81
50
  ### Lifting a suppression
82
51
 
83
- Lift one only when you know the address works again, such as a reader who tells you their mailbox is fixed. Mailing an address that keeps failing can hurt every send after it. In a subscriber's ⋯ menu, **Unblock** lifts a block and **Clear** lifts a bounce, after asking you to confirm. A complaint isn't a suppression but the reader leaving, so nothing lifts it: they come back only by subscribing again themselves, and **Add subscriber** refuses them. Past records keep the bounce or complaint where it happened.
52
+ Lift one only when you know the address works again, such as a reader who tells you their mailbox is fixed. Mailing an address that keeps failing can hurt every send after it. In a subscriber's ⋯ menu, **Unblock** lifts a block and **Clear** lifts a bounce, after asking you to confirm. A complaint isn't a suppression but the reader leaving, so nothing lifts it. They come back only by subscribing again themselves, and **Add subscriber** refuses them. Past records keep the bounce or complaint where it happened.
84
53
 
85
54
  ## An import is refused
86
55
 
87
- Kestrel refuses a file it can't read with certainty, and says what's wrong and where. Nothing on your list changes.
88
-
89
- - **Not in the format.** The file isn't a Kestrel CSV or a plain list. Convert it first, with Claude or by hand.
90
- - **A column or value the format doesn't take,** named with its row and column. Fix it in the file, or move a column of your old service's into `evidence`.
91
- - **Too many rows** for one import. Split the file.
92
- - **Too large.** An import takes up to 10 MB.
93
-
94
- [The file format](01-your-list.md#the-file-format) describes what each column takes.
56
+ Nothing on your list changes. Kestrel refuses a file it can't read with certainty, and names the row and column it couldn't. Fix the file and import it again. Convert a file in another format first, with Claude or by hand. Move a column of your old service's into `evidence`. Split a file of more than 10 MB, or with too many rows. [The file format](01-your-list.md#the-file-format) describes what each column takes.
95
57
 
96
58
  ## Claude can't connect
97
59
 
98
- [Connect Claude](08-connect-claude.md) covers connecting it. When that fails, find what Claude or the dashboard says:
60
+ Most failures are in your instance's Cloudflare Access setup, made by whoever runs your instance. [Connect Claude](08-connect-claude.md) covers connecting it. When that fails:
99
61
 
100
62
  - **Claude says it couldn't connect, or the sign-in failed.** Your instance's Access application needs Managed OAuth set up as [Turn on Managed OAuth](../guides/01-allow-claude.md#1-turn-on-managed-oauth) describes, including Claude's sign-in address for Claude Desktop and claude.ai. Check that the connector's address is your publication's address followed by `/api/mcp`, and that your instance is deployed, not running on your own computer.
101
63
  - `kestrel login` says your instance "doesn't offer a sign-in for programs". Managed OAuth is off. Turn it on, as the same section describes.
102
- - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the address you signed in with. If `kestrel login` names `access_unidentified` after the `401`, or `kestrel api` prints `reason: access_unidentified`, it's the next problem: both then say that Access sent neither an email address nor a service token. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
103
- - **"Access let you in, but didn't say who you are".** The editor shows this, and the API refuses with a `401` whose `reason` is `access_unidentified`, when your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the `Publishers` policy. Whoever runs your instance removes the other policy from its Access application, or changes it to one of those two kinds.
104
- - **Claude's changes read "via the API".** A request made with your sign-in named no client, such as one from Claude calling the API some way other than `kestrel api`, which names Claude Code. Your instance's instructions for Claude Code have it use `kestrel api`, once they're brought up to date, as [Install the release](../guides/07-upgrade.md#2-install-the-release) describes. Changes reading "a service token" mean Claude still uses one, and [Connect Claude](08-connect-claude.md#claude-code) signs it in as you instead.
105
- - A script gets a `401` whose `reason` is `access_unidentified`. Access let the script in through a policy Kestrel doesn't accept. It needs a service token under a Service Auth policy, as [Allow the token in](../guides/02-automate.md#2-allow-the-token-in) describes.
64
+ - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the address you signed in with. If either names `access_unidentified`, it's the next problem. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
65
+ - **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the `Publishers` policy. Whoever runs your instance removes the other policy from its Access application, or changes it to one of those two kinds. A script that gets this needs a service token under a Service Auth policy, as [Allow the token in](../guides/02-automate.md#2-allow-the-token-in) describes.
66
+ - **Claude's changes read "via the API".** Claude called the API some way other than `kestrel api`, which names Claude Code. Your instance's instructions for Claude Code have it use `kestrel api` once they're brought up to date, as [Install the release](../guides/07-upgrade.md#2-install-the-release) describes. Changes reading "a service token" mean Claude still uses one, and [Connect Claude](08-connect-claude.md#claude-code) signs it in as you instead.
106
67
 
107
68
  ## Notifications
108
69
 
@@ -10,7 +10,7 @@ Choose **Template** in the sidebar, then **Send test email**, and send a sample
10
10
 
11
11
  - **The From name and address,** which readers see first. They're set when your instance is deployed.
12
12
  - **Where it lands:** the inbox, Promotions, or spam. If it lands in spam, check your sending domain's records, as [Sending-domain DNS](../reference/02-sending-domain-dns.md) describes.
13
- - **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/04-ses.md#3-receive-replies-at-your-from-address).
13
+ - **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/04-ses.md#9-receive-replies-at-your-from-address).
14
14
 
15
15
  ## 2. Subscribe yourself
16
16
 
@@ -6,7 +6,7 @@ Every deploy-time setting: what it does, its default, and what the app refuses.
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.
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. The top level turns off `workers_dev` and `preview_urls`, empties `triggers.crons`, and has no route. So a deploy that forgets `--env production` answers nowhere and never sends. Leave them as they are, and keep the empty `crons`, since only an empty list clears a schedule an earlier deploy left.
10
10
 
11
11
  None of these can be read or changed from inside the app, by the editor or by Claude. Settings in the editor shows the public ones, read-only.
12
12
 
@@ -46,10 +46,12 @@ Vars go in the environment block's `vars`. Every value is a string.
46
46
 
47
47
  - `PROVIDER` optional
48
48
 
49
- The email provider: `ses`, or `fake` for local development. Default: none, so no provider is connected. Refused unless it's exactly `ses`, `fake`, or `none`: a misspelling, or `SES`, is never taken for another. `resend`, which earlier releases supported, is refused with a pointer to [Connect Amazon SES](../get-started/04-ses.md#switch-an-instance-that-already-sends).
49
+ The email provider: `ses`, or `fake` for local development. Default: none, so no provider is connected. Refused unless it's exactly `ses`, `fake`, or `none`: a misspelling, or `SES`, is never taken for another. `resend`, which earlier releases supported, is refused with a pointer to [Connect Amazon SES](../get-started/04-ses.md).
50
50
 
51
51
  Until it's set, or while it's empty, the app runs with no provider connected. Posts can be written and previewed, but everything that would send email is refused: scheduling, sending now, test emails, and the confirmation emails that signups need. The subscribe page says signups aren't open yet, and Settings shows **No email provider connected**. Setting it to `none` means the same. A new instance's `production` block declares it empty, so wrangler doesn't warn that the top level's value is missing there. [Connect Amazon SES](../get-started/04-ses.md) changes it to `ses`.
52
52
 
53
+ Change it, or move to another SES account or region, only while no send is sending. The dashboard lists any send in progress.
54
+
53
55
  `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 copy it into `production`, whatever wrangler suggests: every subscriber would be recorded as sent the post, and never get it. The app refuses `fake` unless `APP_ORIGIN` is on `localhost` or `127.0.0.1`, since on a real hostname it would open signups whose confirmations go nowhere.
54
56
 
55
57
  - `ALLOW_FAKE_PROVIDER` optional
@@ -176,7 +178,7 @@ Wrangler doesn't carry bindings or vars over from the top level of `wrangler.jso
176
178
 
177
179
  - `observability` optional
178
180
 
179
- Workers Logs, where the app's log lines land. On in the template. Every invocation is logged, with no sampling, since sampling could drop the one minute that reports a stuck or missed send.
181
+ Workers Logs, where the app's log lines land. On in the template. Kestrel sets no sampling, since sampling could drop the one minute that reports a stuck or missed send, so leave `head_sampling_rate` at its default of 1. Cloudflare's own daily limit on the Free plan can still stop new logs for the rest of the day, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes.
180
182
 
181
183
  - `DB` required
182
184
 
@@ -25,15 +25,15 @@ Every record lives in your domain's DNS, `example.com`, under the sending name.
25
25
 
26
26
  SES generates the DKIM values. Copy them exactly as SES lists them, since they differ by account and region. You write the DMARC record yourself.
27
27
 
28
- SPF needs no record of yours on SES's default path, since it passes for a subdomain of `amazonses.com`. An SPF record, at `bounce.send`, comes only with the optional custom MAIL FROM domain.
28
+ SPF needs no record of yours on SES's default path, since it passes for a subdomain of `amazonses.com`. An SPF record, at `bounce.send`, comes with the custom MAIL FROM domain.
29
29
 
30
- Email Routing adds MX, SPF, and DKIM records at `send` itself, the DKIM one under the selector `cf2024-1`. They're for mail sent *to* your From address, so readers' replies reach the publisher ([Connect Amazon SES](../get-started/04-ses.md#3-receive-replies-at-your-from-address)). They don't affect your sending: inboxes check SES's mail for SPF at its return path, and for DKIM against SES's own keys.
30
+ Email Routing adds MX, SPF, and DKIM records at `send` itself, the DKIM one under the selector `cf2024-1`. They're for mail sent *to* your From address, so readers' replies reach the publisher ([Connect Amazon SES](../get-started/04-ses.md#9-receive-replies-at-your-from-address)). They don't affect your sending: inboxes check SES's mail for SPF at its return path, and for DKIM against SES's own keys.
31
31
 
32
32
  ### SPF: authorize the return path
33
33
 
34
34
  SPF checks the message's return path, the address bounces go back to, not its `From:` header. DMARC passes when SPF or DKIM passes and aligns with the `From:` domain. DKIM aligns, so an aligned SPF is a second check in your favor, not a requirement.
35
35
 
36
- SES uses a subdomain of its own domain, `amazonses.com`, as the return path by default. SPF then passes for it but doesn't align, and DMARC passes through DKIM alone. A custom MAIL FROM domain, such as `bounce.send.example.com`, aligns SPF too. [Connect Amazon SES](../get-started/04-ses.md#4-optional-set-a-custom-mail-from-domain) sets one up, with an MX record that names your region and an SPF record:
36
+ SES uses a subdomain of its own domain, `amazonses.com`, as the return path by default. SPF then passes for it but doesn't align, and DMARC passes through DKIM alone. A custom MAIL FROM domain, such as `bounce.send.example.com`, aligns SPF too. [Connect Amazon SES](../get-started/04-ses.md#2-set-a-custom-mail-from-domain) sets one up, with an MX record that names your region and an SPF record:
37
37
 
38
38
  ```
39
39
  bounce.send.example.com. MX 10 feedback-smtp.us-east-1.amazonses.com.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurtbruns/kestrel",
3
- "version": "1.15.1",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
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",
@@ -48,8 +48,6 @@
48
48
  "seed": "node bin/kestrel.mjs seed",
49
49
  "reset": "node bin/kestrel.mjs reset",
50
50
  "simulate-send": "node scripts/simulate-send.mjs",
51
- "predeploy": "node scripts/stamp-version.mjs",
52
- "deploy": "node scripts/require-env.mjs deploy",
53
51
  "types": "wrangler types",
54
52
  "typecheck": "node scripts/stamp-version.mjs --if-changed && wrangler types && tsc --noEmit && tsc --noEmit -p client",
55
53
  "client:build": "node scripts/build-client.mjs",
@@ -65,7 +63,6 @@
65
63
  "check": "biome check --write .",
66
64
  "ci": "biome ci .",
67
65
  "migrate:local": "wrangler d1 migrations apply DB --local",
68
- "migrate:remote": "node scripts/require-env.mjs d1 migrations apply DB --remote",
69
66
  "check:package": "node scripts/check-package.mjs"
70
67
  },
71
68
  "dependencies": {
@@ -16,6 +16,7 @@ Deploying, migrating the production database, and anything that sends mail or ch
16
16
 
17
17
  - **Follow the guide page, step by step.** Each page is a complete procedure. Do what a step says, ask the person for what only they can do (a dashboard click, a DNS record at their registrar), and end with the page's **Check it** list, reporting each check's result.
18
18
  - **Deploying, migrating, and creating or binding a resource are plain wrangler,** always naming the environment, since the top level is development: `npx wrangler deploy --env production`, `npx wrangler d1 migrations apply DB --remote --env production`, and `npx wrangler d1 create NAME --env production --binding DB --update-config`, which writes the binding into `production` instead of offering to add it to the top level.
19
+ - **Never deploy or store a secret without `--env production`.** Without it, wrangler acts on the top level: a deploy creates a second Worker, named `kestrel`, running the stand-in transport and the local sign-in, and a secret lands on that Worker instead of production. The top level turns off every address that Worker could answer on and its send schedule; keep `workers_dev` and `preview_urls` there `false` and its `crons` present and empty (deleting the line leaves an earlier schedule running), and never add a route there. If a deploy went out without `--env`, tell the person, and remove it with `npx wrangler delete --name kestrel` once they agree.
19
20
  - **Upgrade with the `/upgrade` skill.** It follows the guide's upgrade page.
20
21
  - **Bring a list from another service with the `/migrate-list` skill.** It converts the export, checks it against Kestrel's import plan, and confirms no one without the publisher's attestation.
21
22
  - **Secrets never pass through you.** The person enters each one themselves with `npx wrangler secret put NAME --env production`, which prompts for the value; never ask for a secret in chat. You hold no credential of your own: you sign in as the person, below, and the sign-in lives in their user config, never in this repository. Never write a secret into `wrangler.jsonc`, `.claude/settings.json`, or a commit.
@@ -28,6 +29,7 @@ Deploying, migrating the production database, and anything that sends mail or ch
28
29
  - **Locally,** with `npm run dev` running, add `--local`: it calls this folder's dev server as its stand-in person, via Claude Code, with no sign-in. `GET /api/dev/outbox` shows the email the stand-in caught.
29
30
  - **`npx kestrel api --list` lists the instance's routes** (`--list sends` one resource's), and **`npx kestrel api help <method> <path>` describes one**: its parameters, body fields, answer, and refusals with their next moves. Both read the instance's own OpenAPI document, so they match the release it runs.
30
31
  - **Read `GET /api/reference.md` first:** how to authenticate, what a status and an error body tell you and the refusals any route can give, paging, acting safely, and every route on one line. **Before calling a route, read its resource's reference,** `GET /api/reference/<resource>.md` (the index names it, such as `sends.md`): the object the routes return, and each route's parameters, body, answer, refusals (each with what to do next), and an example. Read a resource's reference once per conversation, not before every call.
32
+ - **Posts with images:** upload each image from a file on disk (an image pasted into the chat isn't one; ask the person to save it into this folder), then reference it in the Markdown by its `filename`, with alt text, `![a short description](cover.jpg)`, never by the upload's `url`. The preview warns on a `url` reference, and on a name the post doesn't have.
31
33
  - **Never overwrite someone else's edit.** A save names the revision it was based on (the posts reference says how). On `stale_revision`, someone else changed the post (the refusal's `author` says who): read it again and re-apply your change to the newer revision, or ask.
32
34
  - **Offer only what a post's or a send's `actions` list,** as the reference's Acting safely says, so a post with no `schedule` or `send_now` (no email provider connected) isn't offered for sending. Read the answer to an act, or its refusal, before telling the publisher what happened or deciding again: it carries the resource as it now stands.
33
35
  - **Resolve a wedged send only on the publisher's choice.** Relay the `wedged` condition's message and ask which `resolution` they want, in the words the editor's Resolve dialog shows them: `unsent` is **Assume not sent**, which puts those readers in front of the next post, and `accepted` is **Assume sent**, which records them as sent. Never choose it yourself.
@@ -19,9 +19,16 @@
19
19
  // start while an origin or a sending setting is still on example.com, provider or not.
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
- // The send sweep, once a minute. `npm run dev` runs it locally, where wrangler never
23
- // fires a cron.
24
- "triggers": { "crons": ["* * * * *"] },
22
+ // Deploy only with `--env production`. A wrangler command without it acts on this
23
+ // development config, which runs the stand-in transport and the local sign-in. These lines
24
+ // keep a deploy of it inert: no address it answers on, and no schedule, so its send sweep
25
+ // never runs, whatever database it is bound to. Leave them as they are, the empty `crons`
26
+ // included: deleting it instead would leave a schedule from an earlier deploy running.
27
+ // `npm run dev` runs the sweep locally on its own clock, and `production` below declares
28
+ // its schedule.
29
+ "workers_dev": false,
30
+ "preview_urls": false,
31
+ "triggers": { "crons": [] },
25
32
 
26
33
  // The editor, built by the release. Requests with no file fall through to the Worker.
27
34
  "assets": {