everyapp 0.2.2 → 0.3.0-rc.2

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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -77
  3. package/bin/everyapp.js +235 -0
  4. package/dist/access-LGEN3ABJ.js +1 -0
  5. package/dist/activity-RSD4D66F.js +12 -0
  6. package/dist/apps-4RCFXT74.js +13 -0
  7. package/dist/chunk-23HJ5I2A.js +2 -0
  8. package/dist/chunk-2Y5Q6A5K.js +6 -0
  9. package/dist/chunk-4JRIS6S6.js +1 -0
  10. package/dist/chunk-5PTNYZK6.js +4 -0
  11. package/dist/chunk-6OGVO7QK.js +6 -0
  12. package/dist/chunk-6PGUCTJ6.js +11 -0
  13. package/dist/chunk-AGTCKFRB.js +22 -0
  14. package/dist/chunk-BHMRY53V.js +2 -0
  15. package/dist/chunk-CB4U7H2G.js +4 -0
  16. package/dist/chunk-CCWBPAKH.js +4 -0
  17. package/dist/chunk-CZTEDWZ2.js +22 -0
  18. package/dist/chunk-CZXHKPRZ.js +2 -0
  19. package/dist/chunk-E3EPK27P.js +10 -0
  20. package/dist/chunk-EGEWRFVY.js +1 -0
  21. package/dist/chunk-ELK7QZ2M.js +1 -0
  22. package/dist/chunk-FJJSSVET.js +33 -0
  23. package/dist/chunk-GKBPYVP3.js +25 -0
  24. package/dist/chunk-H6QQMY7B.js +18 -0
  25. package/dist/chunk-H7PDL3X5.js +1 -0
  26. package/dist/chunk-HOUHG236.js +20 -0
  27. package/dist/chunk-IFCPALVC.js +46 -0
  28. package/dist/chunk-IUOBIB5M.js +30 -0
  29. package/dist/chunk-J35DDCNU.js +1 -0
  30. package/dist/chunk-J455FAEN.js +7 -0
  31. package/dist/chunk-JYKRUS5C.js +1 -0
  32. package/dist/chunk-LXYMDZER.js +1 -0
  33. package/dist/chunk-LYNBZ7RV.js +57 -0
  34. package/dist/chunk-M2QQ2UD7.js +132 -0
  35. package/dist/chunk-N4E5YE4X.js +5 -0
  36. package/dist/chunk-N5GQLGEC.js +2 -0
  37. package/dist/chunk-NFU4YFXB.js +1 -0
  38. package/dist/chunk-OM2HDWVP.js +1 -0
  39. package/dist/chunk-ONLNJXVQ.js +32 -0
  40. package/dist/chunk-OP4DYXYI.js +2 -0
  41. package/dist/chunk-PNLU5U3P.js +1 -0
  42. package/dist/chunk-PS7S74DG.js +6 -0
  43. package/dist/chunk-RMF4YGD6.js +98 -0
  44. package/dist/chunk-T2GD7RHS.js +10 -0
  45. package/dist/chunk-TLRM3P7V.js +15 -0
  46. package/dist/chunk-W3JCQHHU.js +1 -0
  47. package/dist/chunk-WOVS3GV5.js +10 -0
  48. package/dist/chunk-YHJZMJYZ.js +1 -0
  49. package/dist/chunk-ZECBN24L.js +15 -0
  50. package/dist/chunk-ZEXVHB22.js +2 -0
  51. package/dist/chunk-ZFCUH5PK.js +7 -0
  52. package/dist/configure-F7UZCJUG.js +8 -0
  53. package/dist/create-2E5HVWYK.js +22 -0
  54. package/dist/db-HEQVTDTM.js +9 -0
  55. package/dist/delete-XCSIQIRS.js +3 -0
  56. package/dist/deploy-3QOZEZHC.js +1 -0
  57. package/dist/dev-JL3ZP5BT.js +86 -0
  58. package/dist/email-RLZFUFUN.js +10 -0
  59. package/dist/events-2V5VRBWP.js +15 -0
  60. package/dist/groups-HDG5DVFY.js +8 -0
  61. package/dist/index.js +97 -1
  62. package/dist/install-Z5FYXUYM.js +1 -0
  63. package/dist/login-KHJBC64Z.js +1 -0
  64. package/dist/mcp-EFDUKJ5V.js +1 -0
  65. package/dist/provider-EOI5PTFH.js +13 -0
  66. package/dist/release-74J5ZIL3.js +1 -0
  67. package/dist/restore-FFGYV7NO.js +3 -0
  68. package/dist/secret-7FKKTLHB.js +1 -0
  69. package/dist/setup-PAKRCAFO.js +1 -0
  70. package/dist/signin-AP3D4XM4.js +12 -0
  71. package/dist/status-RMWNHOEK.js +4 -0
  72. package/dist/teardown-E4JNKAZJ.js +22 -0
  73. package/dist/update-DWVZFNPS.js +1 -0
  74. package/dist/workspace-CJC5I66L.js +21 -0
  75. package/package.json +19 -16
  76. package/skills/build-every-app/SKILL.md +199 -0
  77. package/skills/build-every-app/patterns.md +140 -0
  78. package/skills/everyapp/SKILL.md +292 -0
  79. package/skills/everyapp/mcp.md +350 -0
  80. package/skills/everyapp/security-review.md +61 -0
  81. package/skills/everyapp/setup/README.md +274 -0
  82. package/skills/everyapp/setup/access.md +178 -0
  83. package/skills/everyapp/setup/context.md +117 -0
  84. package/skills/everyapp/setup/dataforseo.md +287 -0
  85. package/skills/everyapp/setup/email.md +366 -0
  86. package/skills/everyapp/setup/gateway.md +243 -0
  87. package/skills/everyapp/setup/google.md +360 -0
  88. package/skills/everyapp/setup/microsoft-365.md +281 -0
  89. package/skills/everyapp/setup/openai.md +318 -0
  90. package/skills/everyapp/setup/openrouter.md +309 -0
  91. package/skills/everyapp/setup/providers.md +1253 -0
  92. package/skills/everyapp/setup/remote-mcp.md +199 -0
  93. package/skills/everyapp/setup/signin-apple.md +178 -0
  94. package/skills/everyapp/setup/signin-cloudflare.md +148 -0
  95. package/skills/everyapp/setup/signin-github.md +139 -0
  96. package/skills/everyapp/setup/signin-google.md +178 -0
  97. package/skills/everyapp/setup/signin-microsoft.md +183 -0
  98. package/skills/everyapp/setup/signin-oidc.md +163 -0
  99. package/skills/everyapp/setup/slack.md +344 -0
  100. package/skills/everyapp/setup/workers-ai.md +138 -0
  101. package/bin/__everyapp_bash_complete.js +0 -12
  102. package/dist/bash-complete.js +0 -3
  103. package/dist/chunk-7BOQJFI5.js +0 -8
  104. package/dist/chunk-7YVJFS4A.js +0 -1
  105. package/dist/chunk-BIXKPCDH.js +0 -2
  106. package/dist/chunk-DLGPLHBH.js +0 -47
  107. package/dist/chunk-FD6K2YA7.js +0 -9
  108. package/dist/chunk-GAJEQYJH.js +0 -39
  109. package/dist/chunk-O2KDOP5R.js +0 -13
  110. package/dist/chunk-OI2MBGBG.js +0 -4
  111. package/dist/chunk-POSDLD3G.js +0 -8
  112. package/dist/chunk-RB5D2R45.js +0 -1
  113. package/dist/chunk-U4LIQ5VR.js +0 -1
  114. package/dist/chunk-VEYSNPDH.js +0 -3
  115. package/dist/chunk-VP6SKW44.js +0 -7
  116. package/dist/chunk-WO3NE7ZU.js +0 -7
  117. package/dist/chunk-XTO2VTPY.js +0 -6
  118. package/dist/chunk-YJ2DKIZS.js +0 -1
  119. package/dist/create-DYQ3NBWJ.js +0 -22
  120. package/dist/deploy-PYTCMDGV.js +0 -7
  121. package/dist/deploy-RMAHOA4Z.js +0 -55
  122. package/dist/dev-SAE237FS.js +0 -0
  123. package/dist/generateConfig-46Z3ERF3.js +0 -1
  124. package/dist/login-B6EF3SMX.js +0 -1
  125. package/dist/logout-XMKZBLNV.js +0 -1
  126. package/dist/remoteD1Shell-A67JMO3H.js +0 -6
  127. package/dist/setupLocal-TZJUW5ES.js +0 -6
  128. package/gateway/every-app-gateway-build.tar.gz +0 -0
@@ -0,0 +1,366 @@
1
+ # Email sending for the Gateway
2
+
3
+ Lets the Gateway send its own mail (invitations, password resets, sign-up
4
+ verification) and every app's, through one provider for the whole Gateway:
5
+ Cloudflare Email Sending (the default) or Resend. Apps that send: agentic-inbox
6
+ (replies from its own addresses; required), Open Analytics (reports and
7
+ alerts; `optional: true`, so it opens without it) and any app whose
8
+ `everyapp.config.ts` lists `capabilities: { email: { access: "write" } }`.
9
+ Email is optional: without it, an invite shows its link to share instead. At the end the person
10
+ has a sending domain set up with the provider, `EMAIL_FROM` and
11
+ `EMAIL_FROM_NAME` on the Gateway, and an invitation that arrived by mail.
12
+
13
+ ## Settings
14
+
15
+ | NAME | required or optional | what it holds |
16
+ | ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
17
+ | `EMAIL_FROM` | required | the sender address, `noreply@<your domain>`; or the combined form `Acme <noreply@<your domain>>`, the display name inside it (quotes around the name are dropped) |
18
+ | `EMAIL_FROM_NAME` | required, unless `EMAIL_FROM` carries the name | the display name people see, e.g. `Acme`; set, it outranks the name inside `EMAIL_FROM` |
19
+ | `RESEND_API_KEY` | optional (Resend only) | a Resend API key, `re_…`, with Sending access to every domain the Gateway sends as (step 6) |
20
+ | `EMAIL_PROVIDER` | optional | `cloudflare` or `resend` (any case): pins the provider; unset, the Gateway picks one (below) |
21
+ | `EMAIL_REST_API_TOKEN` | optional (a Gateway outside Workers: local dev) | a Cloudflare API token with Email Sending Edit, with `CLOUDFLARE_ACCOUNT_ID`; never set it on a deployed Gateway: it outranks the `EMAIL` binding |
22
+
23
+ Set both, the default. Or `EMAIL_FROM` alone in the combined form, with
24
+ `EMAIL_FROM_NAME` unset. Either way the Gateway sends only once it has an
25
+ address and a name (`parseEmailSender` in `packages/sdk/src/internal/email.ts`;
26
+ the Gateway's `configuredSender()` uses it): a bare address with no name
27
+ anywhere counts as no sender, so the Gateway says "Email is not configured"
28
+ and an app's send gets `integration_not_configured` (503). `secret list`
29
+ and `status` read names, not values: an app's `email` setting counts as set
30
+ once `EMAIL_FROM` and `EMAIL_FROM_NAME` are both on the Gateway (both
31
+ `required: true` in `data.gateway.needed`), so the combined form with no
32
+ `EMAIL_FROM_NAME` reads as `! not set` there while the Gateway, which reads
33
+ the values (`isEmailConfigured`), opens the app and sends. Verify 2 is the
34
+ check that counts.
35
+
36
+ How the provider is picked: `EMAIL_PROVIDER` when set (and it must be set up;
37
+ it never falls back); else Cloudflare when `EMAIL_REST_API_TOKEN` is set, else
38
+ Resend when `RESEND_API_KEY` is set, else Cloudflare's `EMAIL` binding, which
39
+ every deployed Gateway has. So a deployed Gateway with `RESEND_API_KEY` sends
40
+ through Resend without `EMAIL_PROVIDER`. Neither `EMAIL_FROM` nor
41
+ `EMAIL_FROM_NAME` is a secret: pipe them with `echo`.
42
+
43
+ ## Costs and plans
44
+
45
+ As of October 2026. Cloudflare Email Sending (in beta): "Sending to arbitrary
46
+ recipients requires the Workers Paid plan" ($5 a month, which `everyapp setup`
47
+ already needs); 3,000 emails a month included, then $0.35 per 1,000. Before a
48
+ domain is onboarded, mail goes only to verified destination addresses (those
49
+ sends are free and uncounted on any plan). New accounts start with "a
50
+ conservative daily quota" that grows with sending behaviour (Verify 6 reads
51
+ it). 50 recipients and 5 MiB a message. Resend Free: 3,000 a month, 100 a
52
+ day, 3 domains, 30 days of logs; Pro is $20 a month for 50,000. Resend's
53
+ pages don't say whether Free needs a card. Resend's default rate limit is 10
54
+ requests a second per team, all keys together (raised on request at
55
+ resend.com/help). The Gateway's own budget for app mail: 100 a day per
56
+ recipient address (20 on a hosted Gateway) and 5,000 a day per Gateway.
57
+
58
+ ## Only a person can
59
+
60
+ - Pay for Workers Paid, or create a Resend account, accept its terms and pick
61
+ a plan.
62
+ - Click **Onboard Domain** in the Cloudflare dashboard (you can do this with
63
+ `cf` instead, step 2).
64
+ - Create the Resend API key and copy it off the screen into the hidden prompt.
65
+ - Approve Resend's **Sign in to Cloudflare** (Domain Connect) if they use it.
66
+ - Say yes to turning Email Routing on for a domain (its MX and SPF records
67
+ move to Cloudflare; mail hosted elsewhere stops arriving).
68
+ - Open their inbox and confirm the invitation or reset mail arrived.
69
+
70
+ ## Before you start
71
+
72
+ - `pnpm everyapp secret list <app id> --json` (`agentic-inbox` or
73
+ `analytics`, the launch apps that list `email`; from anywhere once it is
74
+ deployed, else `pnpm everyapp secret list --json` in its directory): this
75
+ file applies when `data.app.setup.unmet`
76
+ has the entry `id: "email"` (`kind: "capability"`, `holder: "gateway"`,
77
+ `names: ["EMAIL_FROM", "EMAIL_FROM_NAME"]`, `need.kind: "required"`, `setup: "setup/email.md"`:
78
+ the path `status` (under "<app> needs setup") and `secret list <app id>`
79
+ (under the unmet setting) print as `Guide:
80
+ .claude/skills/everyapp/setup/email.md`, and `secret list --json` carries
81
+ per unmet setting; how an agent lands here), and `data.app.setup.command`
82
+ is `pnpm everyapp configure <app id>`, which asks for `EMAIL_FROM` and
83
+ `EMAIL_FROM_NAME` among what the app still needs (visible, checked
84
+ prompts; step 8 says what to type). An app
85
+ that marks `email` `optional: true` (Open Analytics) has the entry in
86
+ `settings` with `set: false`, never in `unmet`: it opens without a
87
+ sender, and this file applies when they want its mail. The older
88
+ `data.gateway.needed` (each `{ name, required, set }`) and
89
+ `data.gateway.missing` still say the same of `EMAIL_FROM` and
90
+ `EMAIL_FROM_NAME`; `RESEND_API_KEY`, `EMAIL_PROVIDER` and
91
+ `EMAIL_REST_API_TOKEN` are read when set (`required: false`), never in
92
+ `unmet` or `missing`. The entry `set: true`: both are on the Gateway
93
+ already; ask before replacing them.
94
+ - `pnpm everyapp status --json`: `data.installed` and `data.claimed` true,
95
+ `data.gatewayUrl` gives `<gateway host>`, `data.accountId` is `<account id>`
96
+ (the sending domain's zone must be in this account), `data.apps[].setup`
97
+ says which apps still need setup (an `APP_NEEDS_SETUP` fix each, severity
98
+ `warning`), and each `data.apps[].email.senders` says which apps already
99
+ send as their own.
100
+ - `pnpm everyapp secret list --json` from the checkout root: whether
101
+ `EMAIL_FROM`, `EMAIL_FROM_NAME`, `RESEND_API_KEY`, `EMAIL_PROVIDER` or
102
+ `EMAIL_REST_API_TOKEN` are in `data.gateway.names` (the one place the last
103
+ four show as set). Any set: ask before replacing. `EMAIL_REST_API_TOKEN`
104
+ set on a deployed Gateway: it outranks the binding; ask whether that is meant.
105
+ - Every `cf …` below is the CLI's pinned cf, the one `pnpm everyapp login
106
+ --json` signs in: README's "Where you are" says how to run it.
107
+ - `cf zones list --name <your domain>`: one zone, `status` active, in
108
+ `<account id>`. None: the person adds the domain to Cloudflare first (the
109
+ domain page in the public docs).
110
+ - `cf email-sending subdomains list -z <your domain>`: whether the domain is
111
+ onboarded already (its entry, whose `tag` is `<subdomain id>` below).
112
+ Empty: step 2.
113
+ - For an app's senders, in the app's directory: `pnpm everyapp email show --json`:
114
+ `data.app.deployed` true (false: deploy first), `data.declares.sends` true,
115
+ `data.senders` the current list.
116
+
117
+ ## Steps
118
+
119
+ 1. **You:** choose with the person: the provider and the sender. Cloudflare
120
+ when the domain is on Cloudflare DNS in `<account id>` and they want
121
+ nothing else to sign up for (the default; steps 2 to 3). Resend when they
122
+ already use it or want its dashboard (steps 4 to 7). One provider sends
123
+ everything; both can be set up, with `EMAIL_PROVIDER` picking. The
124
+ sender: the address to send from (offer `noreply@<your domain>`, on the
125
+ domain step 2 or 4 sets up: the apex or `mail.<your domain>`) and the
126
+ name people see (their company name); set neither before they chose.
127
+ Then step 8 for either.
128
+
129
+ **Cloudflare Email Sending**
130
+
131
+ 2. **You:** first, read-only,
132
+ `cf email-sending subdomains preview -z <your domain> --name <your domain>`:
133
+ the records onboarding needs and any conflict (a foreign MX, a second
134
+ SPF) for the person to clean up under **DNS** > **Records** first. Then
135
+ onboard the sending domain in the Gateway's account:
136
+ `cf email-sending subdomains create -z <your domain> --name <your domain>`
137
+ (`--name` is the domain `EMAIL_FROM` uses: the apex, or `mail.<your domain>`;
138
+ never the full address). It turns zone-level Email Sending on too when
139
+ the account has the entitlement (Workers Paid; refused for it: gateway.md,
140
+ step 2). Note the subdomain's `tag` in the output: that is
141
+ `<subdomain id>` below. Alternative,
142
+ **Person:** https://dash.cloudflare.com/?to=/:account/email-service/sending
143
+ (**Compute** > **Email Service** > **Email Sending**; the public docs say
144
+ **Compute & AI**) > **Onboard Domain** > pick the domain > review the
145
+ records > **Done** (**Add records and onboard** in the public docs). Wait
146
+ until the domain is listed under Email Sending.
147
+ 3. **You:** check the DNS records. Dashboard onboarding adds them itself (MX
148
+ and SPF at `cf-bounce.<your domain>`, DKIM at
149
+ `cf-bounce._domainkey.<your domain>`, DMARC at `_dmarc.<your domain>`);
150
+ whether step 2's API path does too is unverified, so look:
151
+ `cf email-sending subdomains dns status <subdomain id> -z <your domain>`
152
+ diffs the expected records against the zone. Any missing or unlocked:
153
+ `cf email-sending subdomains dns fix <subdomain id> -z <your domain>`
154
+ creates them (idempotent; a 409 is a foreign MX or a duplicate
155
+ SPF/DKIM/DMARC record at those names, for the person to clean up under
156
+ **DNS** > **Records**). Run `dns status` again until nothing is missing,
157
+ usually within 5 to 15 minutes. Go to step 8.
158
+
159
+ **Resend**
160
+
161
+ 4. **Person:** https://resend.com/domains > **Add Domain** > type the domain
162
+ `EMAIL_FROM` will use (Resend recommends a subdomain, `mail.<your domain>`)
163
+ > the region closest to recipients > add it. Fastest: Resend's **Sign in
164
+ to Cloudflare** button (Domain Connect): they approve once, the records
165
+ land in the zone, and the domain verifies within minutes; skip step 5's
166
+ records. Else the **Records** tab lists a DKIM TXT record at
167
+ `resend._domainkey` and SPF records at `send` (an MX, priority 10, and a
168
+ TXT); DMARC is recommended, not required. They paste the names and
169
+ values to you (public DNS values, not secrets): `<dkim value>`,
170
+ `<mx host>` and `<spf value>` below.
171
+ 5. **You:** add the records to the zone, `DNS only`, one
172
+ `cf dns records create -z <your domain> --body '…'` per record, each with
173
+ its complete name (the record it prints shows the `name` created):
174
+ `{"type":"TXT","name":"resend._domainkey.<your domain>","content":"<dkim value>","ttl":1}`,
175
+ `{"type":"MX","name":"send.<your domain>","content":"<mx host>","priority":10,"ttl":1}`,
176
+ `{"type":"TXT","name":"send.<your domain>","content":"<spf value>","ttl":1}`
177
+ (under `mail.<your domain>`: `resend._domainkey.mail.<your domain>` and
178
+ `send.mail.<your domain>`; a free priority if 10 is taken). Then
179
+ **Person:** on the domain's page, **Verify DNS Records**; the status goes
180
+ **Pending** > **Verified** (often within 15 minutes, up to 72 hours;
181
+ **Failed** after that: **Restart verification**). Until then Resend
182
+ refuses mail from it.
183
+ 6. **Person:** https://resend.com/api-keys > **Create API Key** > Name
184
+ `every-app-gateway` > Permission **Sending access** > Domain: leave it
185
+ unrestricted if any app will send as its own domain (step 9), else the
186
+ domain from step 4 (both are editable there later; the key's value is
187
+ not) > create. The key (`re_…`) is shown once. README's first way: with
188
+ it on screen, they open a second terminal in the `every-app` folder, run
189
+ `pnpm everyapp secret set RESEND_API_KEY`, paste at the hidden prompt,
190
+ and tell you. Or README's third way: exported before they started you,
191
+ you run `pnpm everyapp secret set RESEND_API_KEY --from-env RESEND_API_KEY --json`;
192
+ else they read it in hidden in their own terminal (`read -rs
193
+ RESEND_API_KEY && export RESEND_API_KEY`; never a typed `export …=`,
194
+ which the shell history keeps), run that line there, then
195
+ `unset RESEND_API_KEY`.
196
+ 7. **You:** pin the provider only when both are set up, or the person wants
197
+ it explicit: `echo resend | pnpm everyapp secret set EMAIL_PROVIDER --json`
198
+ (README's second way; `resend` is not a secret). Look for `done`.
199
+
200
+ **The sender, either provider**
201
+
202
+ 8. **You:** the address and name chosen in step 1, one value per pipe
203
+ (README's second way; two names in one pipe is `SECRET_SEVERAL_NAMES`):
204
+ `echo "noreply@<your domain>" | pnpm everyapp secret set EMAIL_FROM --json`
205
+ then `echo "Acme" | pnpm everyapp secret set EMAIL_FROM_NAME --json`, with
206
+ their address and name in place of the examples. Or, when the person
207
+ wants one setting, the combined form (Settings):
208
+ `echo "Acme <noreply@<your domain>>" | pnpm everyapp secret set EMAIL_FROM --json`
209
+ and no `EMAIL_FROM_NAME`. Each ends with `done`; the Gateway reads the
210
+ new values on its next request, no deploy. Then Verify 1:
211
+ `pnpm everyapp secret list --json`, both names in `data.gateway.names`
212
+ (`EMAIL_FROM` alone with the combined form; and `RESEND_API_KEY` for
213
+ Resend). Or, at the person's terminal, `pnpm everyapp configure <app id>`
214
+ (`agentic-inbox` or `analytics`) asks for `EMAIL_FROM` and then
215
+ `EMAIL_FROM_NAME` among the app's other settings, at visible prompts
216
+ that say what each takes and check it (the address, then the name, as
217
+ in the two pipes above; where `email` is required it wants both, so the
218
+ combined form alone is for the pipe).
219
+
220
+ **An app that sends as its own addresses** (agentic-inbox)
221
+
222
+ 9. **You:** after the app's first deploy, set up its domain with the same
223
+ provider (steps 2 to 3, or 4 to 5; a domain done for `EMAIL_FROM` needs
224
+ nothing). Resend: the key must cover the new domain too; one restricted
225
+ to step 4's domain refuses mail from any other, so the person edits its
226
+ Domain at https://resend.com/api-keys (the value stays; nothing to
227
+ re-set) or creates a new key (step 6, `secret set RESEND_API_KEY` again).
228
+ Then in the app's directory:
229
+ `pnpm everyapp email set --sender "*@<your domain>" --json`. `done` with
230
+ `data.senders.value` the new list and `data.senders.changed` true.
231
+ `--sender` replaces the whole list (repeat it for several; one address
232
+ works too); the Gateway picks it up within 30 seconds. Until it has one
233
+ the app sends as the Gateway's sender, and the CLI says so: its
234
+ `pnpm everyapp deploy --json` ends with `data.next` naming
235
+ `pnpm everyapp email set --sender '*@<domain>'` ("lets <app> send as
236
+ your domain"), and `pnpm everyapp email show --json` carries the same
237
+ command in `data.next` ("send as your own domain") under "<app> sends
238
+ as the Gateway's sender"; neither on a hosted Gateway, which takes no
239
+ app senders. An app deployed from a release has no directory here:
240
+ clone its repository and run the command in that directory (only
241
+ `everyapp.config.ts` is read), or pass
242
+ `--manifest <path to its everyapp.config.ts>` from here.
243
+
244
+ **Inbound mail** (an app declaring `email: { inbound: true }`): in its
245
+ directory, `pnpm everyapp email set --inbound "*@<your domain>" --json`
246
+ (an apex catch-all) or one address; the zone must be in `<account id>`.
247
+ `EMAIL_ROUTING_DISABLED`: Email Routing is off for that domain; tell the
248
+ person what turning it on does (above) and rerun with
249
+ `--enable-email-routing` only when they said yes. Then
250
+ `pnpm everyapp status --json`: `data.fixes` has no `EMAIL_*` code (all
251
+ four are about inbound routes; each names what to run, usually
252
+ `pnpm everyapp deploy --json`). The checks and the manual path:
253
+ docs/runbooks/email-sending-setup.md, "Inbound mail to an app".
254
+
255
+ ## Verify
256
+
257
+ 1. `pnpm everyapp secret list agentic-inbox --json` (or the app that brought
258
+ you here, from anywhere): the `email` entry in `data.app.setup.settings` is
259
+ `set: true` and out of `data.app.setup.unmet`, `data.app.setup.ready` true
260
+ once nothing else is missing, `EMAIL_FROM` and `EMAIL_FROM_NAME` out of
261
+ `data.gateway.missing` (with the combined form and no `EMAIL_FROM_NAME`,
262
+ that name stays in it: this reads names). From the checkout root with no
263
+ app named: `EMAIL_FROM` and `EMAIL_FROM_NAME` in `data.gateway.names`
264
+ (`EMAIL_FROM` alone with the combined form; `RESEND_API_KEY` too for
265
+ Resend). Then `pnpm everyapp status --json`: `data.apps[].setup.ready` true
266
+ for that app and no `APP_NEEDS_SETUP` fix naming it. All of this reads
267
+ names: the combined form with no `EMAIL_FROM_NAME` fails here while the
268
+ Gateway sends; Verify 2 is the check that counts.
269
+ 2. Smoke check, by the person (`GET /api/integrations/email` is for apps, not
270
+ operators): on `https://<gateway host>/`, **Manage workspace** (bottom of
271
+ the sidebar) > **Users** > **Invite user** > an address they control >
272
+ **Invite**. The invitation arrives from `EMAIL_FROM_NAME <EMAIL_FROM>` (the
273
+ combined form: `EMAIL_FROM` as set). An
274
+ **Invite link** dialog instead (the link with a copy button, "Send this
275
+ link to <address>…") means it didn't; the Users page's banner "Email is
276
+ not configured." says which case (If it fails). Or `https://<gateway host>/forgot-password`
277
+ with their address: the reset mail arrives (unless `EMAIL_PASSWORD_SIGN_IN`
278
+ is `false`).
279
+ 3. The provider alone, Cloudflare, with the person's yes (one counted
280
+ email, unless `to` is a verified destination address):
281
+ `cf email-sending send --body '{"from":"noreply@<your domain>","to":"<address>","subject":"Every App test","text":"It works."}'`
282
+ sends without the Gateway. It arriving means the provider is done and a
283
+ failed invite is the Gateway's side (its settings; the logs below). A
284
+ refusal is the REST API's numeric code (`10203` sending disabled,
285
+ `10105` not entitled, `10004` throttled), not the binding's `E_*`; an
286
+ un-onboarded sender's code isn't documented. Resend: the domain's
287
+ **Verified** is the only check short of sending (a Sending-access key
288
+ can't list domains: `restricted_api_key`).
289
+ 4. The app: in its directory `pnpm everyapp email show --json` lists each
290
+ pattern in `data.senders` (none yet, and the app declares sending on a
291
+ self-hosted Gateway: `data.next` names `email set --sender '*@<domain>'`,
292
+ step 9); `pnpm everyapp status --json` shows it in
293
+ `data.apps[].email.senders`; the app itself sees `ready: true` and
294
+ `provider` from `integrationStatus({ env, provider: "email" })`. Then
295
+ send one mail from the app.
296
+ 5. Logs, when a send fails: cf 1.0.0-beta.14 has no `workers tail` (`cf
297
+ workers --help`; `cf o11y telemetry live-tail` only prepares a websocket),
298
+ so use the dashboard, README's path:
299
+ **Workers & Pages** → `every-app-gateway` → **Observability** (**Logs** on older accounts) → **Live**,
300
+ then trigger the send. A failure
301
+ is one line, "Email delivery failed", with `event: "email.send.failed"`,
302
+ `kind`, `provider` (which one refused), `code`, `providerCode` and
303
+ `message`; an app's send logs `email.app.send`. Or
304
+ `npx wrangler tail every-app-gateway --format pretty` streams the same
305
+ (npx fetches wrangler, not in the checkout; it signs in apart from cf:
306
+ `npx wrangler login`, which the person approves in the browser).
307
+ 6. Cloudflare's quota and usage: `cf email-sending limits get`:
308
+ `quota.value` a day (`quota.unit`), `usage.sent` so far,
309
+ `usage.over_quota`; `quota` null means none is assigned yet (a new
310
+ account).
311
+
312
+ ## If it fails
313
+
314
+ | error or symptom | meaning | what to do |
315
+ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
316
+ | `SECRET_VALUE_REQUIRED` | nothing was piped and there is no terminal | one of README's three ways |
317
+ | `SECRET_VALUE_REQUIRED` from `pnpm everyapp configure <app id> --json` | nothing was piped (with `--from-env`, stdin isn't read), no variable of a missing name was found, and there is no terminal; the message names what the app still needs (`EMAIL_FROM` and `EMAIL_FROM_NAME` among them while unset) | `NAME=value` lines on stdin (`EMAIL_FROM="Acme <noreply@<your domain>>"`, quoted), or the person's terminal (step 8) |
318
+ | `SECRET_SEVERAL_NAMES` | a pipe or `--from-env` with two names | one `secret set` per name |
319
+ | `SECRET_EMPTY` | the `--from-env` variable isn't set in the shell that ran the command (exported in theirs, not before you started) | they run it in their terminal, or the hidden prompt (step 6) |
320
+ | `SETTING_VALUE_INVALID` with `data.name` | `EMAIL_FROM`, `EMAIL_FROM_NAME` or `EMAIL_PROVIDER` got a value the Gateway wouldn't read (`EMAIL_FROM` takes an address or `Name <address>`; `EMAIL_PROVIDER` `cloudflare` or `resend`); nothing was written | pipe the value `data.fix` describes |
321
+ | `SECRET_NOT_DECLARED` with `data.didYouMean` | the name isn't a Gateway setting (a typo) | rerun with the name in `data.didYouMean` and the same value source (`--from-env VAR --json`, or the same pipe); `data.rerun` carries only the names, so run as is it stops with `SECRET_VALUE_REQUIRED` |
322
+ | `SECRET_NOT_SETTABLE` | a name setup owns (`CLOUDFLARE_ACCOUNT_ID`, written by `everyapp setup`) or a dev-only one | nothing to set; use the names in Settings |
323
+ | `CONFIRMATION_REQUIRED` from `secret unset --json` | it needs `--yes` | ask the person, then rerun with it |
324
+ | the **Invite link** dialog instead of a sent mail, and the Users page's banner "Email is not configured."; app gets `integration_not_configured` (503) | no sender (an `EMAIL_FROM` with no name in it and no `EMAIL_FROM_NAME` counts as none), or no provider is set up, or `EMAIL_PROVIDER` names one that isn't | set `EMAIL_FROM` and `EMAIL_FROM_NAME`, or `EMAIL_FROM` in the combined form (step 8); set that provider's setting or unset `EMAIL_PROVIDER` |
325
+ | the **Invite link** dialog with no banner on the Users page | configured, but the provider refused the send | Verify 5's `email.send.failed` line, then the provider rows below |
326
+ | the app's host answers 503 `app_needs_setup`, or a browser lands on `https://<gateway host>/?setup=<app id>`; `status` has `APP_NEEDS_SETUP` naming `EMAIL_FROM`, or the Gateway's app list says "Needs setup" while `secret list` says `set: true` | the Gateway keeps an app that lists `email` closed until it can send: a sender with a name and a provider (Settings) | step 8 (both settings, or the combined form), then the provider's steps; Verify 2 |
327
+ | log: `EMAIL_PROVIDER is "…", which isn't an email provider` | a value other than `cloudflare` or `resend` | `echo cloudflare \| pnpm everyapp secret set EMAIL_PROVIDER --json`, or unset it |
328
+ | `email_sender_not_verified` (422); `E_SENDER_NOT_VERIFIED`, `E_SENDER_DOMAIN_NOT_AVAILABLE`, `E_RECIPIENT_NOT_ALLOWED` (before onboarding, only verified destination addresses are allowed); Resend 403 naming the domain; mail reaches only verified addresses | the sender's domain isn't onboarded or Verified with the provider | steps 2 to 3 (Cloudflare) or 4 to 5 (Resend) for that domain; `dns status` or Resend's Records tab |
329
+ | `email_provider_limited` (429); `E_DAILY_LIMIT_EXCEEDED`, `E_RATE_LIMIT_EXCEEDED`, `10004`; Resend `daily_quota_exceeded`, `monthly_quota_exceeded`, `rate_limit_exceeded` | the provider's quota or rate | wait; `cf email-sending limits get`, Cloudflare's limit increase form; Resend Pro |
330
+ | `E_ACCOUNT_SUSPENDED` | Cloudflare paused the account's sending (reputation) | Cloudflare support; no self-service path is documented |
331
+ | `email_budget_exhausted` (429) | the Gateway's own daily app-mail budget | wait until UTC midnight (`Retry-After`) |
332
+ | `email_send_failed` (502); log `Resend refused the API key` (the stable marker; `providerCode` is Resend's name: `validation_error` "API key is invalid", `restricted_api_key`, `suspended_api_key`, `invalid_permission`) | the key is wrong, deleted, suspended, or restricted to another domain; any other `providerCode`: the provider refused it | a new key (step 6) and `secret set RESEND_API_KEY` again, or widen its Domain (step 9); else read `providerCode` in the log |
333
+ | `email_sender_not_allowed` (403) | the app's `from` matches none of its senders | step 9 |
334
+ | `EMAIL_NOT_DECLARED` | `everyapp.config.ts` doesn't list `capabilities.email` with `access: "write"` | add it; `pnpm everyapp deploy --json` in the app's directory, then rerun |
335
+ | `APP_NOT_FOUND` from `email set`; `email show`: "isn't deployed yet" | no first deploy | `pnpm everyapp deploy --json` in the app's directory, then rerun |
336
+ | `EMAIL_SENDER_INVALID`; `EMAIL_SENDERS_HOSTED` | a pattern that isn't lowercase `*@<domain>` or one address; a hosted Gateway takes no app senders | fix the pattern (its `data.rerun` lowercases it); apps send as the Gateway's sender |
337
+ | `EMAIL_ROUTING_DISABLED`, `EMAIL_ZONE_NOT_FOUND`, `EMAIL_ADDRESS_TAKEN` | inbound: routing off (needs a yes), zone not in the account, address routed elsewhere | the runbook's inbound section |
338
+ | mail lands in spam | no DMARC, or a new domain with no reputation | Cloudflare adds `_dmarc` itself; add Resend's recommended DMARC record; keep volume low at first |
339
+
340
+ ## Rerun, rotate, remove
341
+
342
+ - Rerun: every step repeats safely. `subdomains create` re-enables a domain
343
+ that exists, `dns fix` is idempotent, `configure` and `secret set`
344
+ replace, `email set --sender` replaces the list. Start again from Verify
345
+ and continue at the first step that isn't done.
346
+ - Rotate Resend: create a new key (step 6), `secret set RESEND_API_KEY`, then
347
+ delete the old key at https://resend.com/api-keys. The Cloudflare binding has
348
+ nothing to rotate. Change the sender: step 8 again.
349
+ - Switch provider: set up the other one, then `EMAIL_PROVIDER`; `pnpm everyapp
350
+ secret unset EMAIL_PROVIDER --json` goes back to the automatic choice.
351
+ - Remove: `pnpm everyapp secret unset RESEND_API_KEY EMAIL_PROVIDER --json`
352
+ (add `--yes` only when the person said so; without it `--json` stops with
353
+ `CONFIRMATION_REQUIRED`), then delete the key at
354
+ https://resend.com/api-keys. Unsetting `EMAIL_FROM` and `EMAIL_FROM_NAME`
355
+ returns the Gateway to invite and reset links. An app's senders:
356
+ `pnpm everyapp email unset senders --json` in its directory. A Cloudflare
357
+ sending domain, only when asked:
358
+ `cf email-sending subdomains delete <subdomain id> -z <your domain>`
359
+ (disables sending and removes its records; it asks, so add `--force` only
360
+ after the person said yes, as with `--yes`), or the dashboard.
361
+
362
+ ## Sources
363
+
364
+ - Cloudflare: https://developers.cloudflare.com/email-service/ (beta, Workers Paid, verified destinations free), https://developers.cloudflare.com/email-service/configuration/domains/ (onboarding, the records it adds), https://developers.cloudflare.com/email-service/get-started/send-emails/ (the dashboard path and deep link, the `EMAIL` binding), https://developers.cloudflare.com/api/resources/email_sending/subresources/subdomains/ (the subdomain's `tag`), https://developers.cloudflare.com/email-service/api/send-emails/workers-api/ (the `E_*` codes), https://developers.cloudflare.com/email-service/api/send-emails/rest-api/ (the numeric codes), https://developers.cloudflare.com/email-service/platform/limits/ (quota, verified destination addresses, 50 recipients, 5 MiB), https://developers.cloudflare.com/email-service/platform/pricing/ (Workers Paid, 3,000 a month, $0.35 per 1,000), https://developers.cloudflare.com/workers/platform/pricing/ ($5 a month), https://developers.cloudflare.com/workers/observability/logs/real-time-logs/ (Logs > Live), https://developers.cloudflare.com/workers/wrangler/commands/workers/ (`wrangler tail`)
365
+ - Resend: https://resend.com/pricing, https://resend.com/docs/api-reference/rate-limit (10 a second per team), https://resend.com/docs/api-reference/errors (error names), https://resend.com/docs/add-a-domain, https://resend.com/docs/dashboard/domains/manage-domains (statuses), https://resend.com/docs/knowledge-base/cloudflare (Sign in to Cloudflare, record names, DNS only, Verify DNS Records), https://resend.com/docs/create-an-api-key, https://resend.com/docs/api-reference/api-keys/create-api-key (Sending access, domain restriction, shown once), https://resend.com/docs/dashboard/api-keys/introduction (permission and domain editable in the Dashboard)
366
+ - Local, 2026-10-23: `cf email-sending subdomains {create,preview,delete,dns status,dns fix} --help`, `cf email-sending limits get` (its fields), `cf dns records create --help` and `cf workers --help` of cf 1.0.0-beta.14 (`packages/cli/package.json` pins it, `lib/cf/bin.ts` runs it, `pnpm --filter everyapp exec cf --version` prints it); docs/runbooks/email-sending-setup.md, docs/capabilities.md and the CLI's error paths (`commands/secret`, `commands/email`, `lib/gateway/registry.ts`, `commands/status/problems.ts`) in the checkout; 2026-10-24: `packages/sdk/src/internal/email.ts` (`parseEmailSender`: the combined form, a name required) and the Gateway's `server/email/sendEmail.ts` (`configuredSender`, `isEmailConfigured`), `commands/deploy/deployApp.ts` and `commands/email/index.ts` (the `email set --sender` next step), `commands/configure/index.ts` (`configure <app>`: the visible prompts for `EMAIL_FROM` and `EMAIL_FROM_NAME`, `NAME=value` lines on stdin), the runbook's "Sender configuration" as of #389; `packages/sdk/src/internal/capabilities.ts` (the `email` entry: `settings: ["EMAIL_FROM"]`, the rest `optionalSettings`; later on 2026-10-24 `EMAIL_FROM_NAME` moved into `settings`, the shape the top of this file describes) and `appSettings.ts`, `packages/cli/src/lib/app/settings.ts` (`gatewayNeeds`, `gatewayMissing`) and `commands/secret/index.ts` (`secret list <app>`), the Gateway's `server/app-setup.ts` (readiness from `isEmailConfigured`) and `server/app-needs-setup.ts` (the closed app), after #383, #395, #396 and #397 merged