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.
- package/LICENSE +21 -0
- package/README.md +6 -77
- package/bin/everyapp.js +235 -0
- package/dist/access-LGEN3ABJ.js +1 -0
- package/dist/activity-RSD4D66F.js +12 -0
- package/dist/apps-4RCFXT74.js +13 -0
- package/dist/chunk-23HJ5I2A.js +2 -0
- package/dist/chunk-2Y5Q6A5K.js +6 -0
- package/dist/chunk-4JRIS6S6.js +1 -0
- package/dist/chunk-5PTNYZK6.js +4 -0
- package/dist/chunk-6OGVO7QK.js +6 -0
- package/dist/chunk-6PGUCTJ6.js +11 -0
- package/dist/chunk-AGTCKFRB.js +22 -0
- package/dist/chunk-BHMRY53V.js +2 -0
- package/dist/chunk-CB4U7H2G.js +4 -0
- package/dist/chunk-CCWBPAKH.js +4 -0
- package/dist/chunk-CZTEDWZ2.js +22 -0
- package/dist/chunk-CZXHKPRZ.js +2 -0
- package/dist/chunk-E3EPK27P.js +10 -0
- package/dist/chunk-EGEWRFVY.js +1 -0
- package/dist/chunk-ELK7QZ2M.js +1 -0
- package/dist/chunk-FJJSSVET.js +33 -0
- package/dist/chunk-GKBPYVP3.js +25 -0
- package/dist/chunk-H6QQMY7B.js +18 -0
- package/dist/chunk-H7PDL3X5.js +1 -0
- package/dist/chunk-HOUHG236.js +20 -0
- package/dist/chunk-IFCPALVC.js +46 -0
- package/dist/chunk-IUOBIB5M.js +30 -0
- package/dist/chunk-J35DDCNU.js +1 -0
- package/dist/chunk-J455FAEN.js +7 -0
- package/dist/chunk-JYKRUS5C.js +1 -0
- package/dist/chunk-LXYMDZER.js +1 -0
- package/dist/chunk-LYNBZ7RV.js +57 -0
- package/dist/chunk-M2QQ2UD7.js +132 -0
- package/dist/chunk-N4E5YE4X.js +5 -0
- package/dist/chunk-N5GQLGEC.js +2 -0
- package/dist/chunk-NFU4YFXB.js +1 -0
- package/dist/chunk-OM2HDWVP.js +1 -0
- package/dist/chunk-ONLNJXVQ.js +32 -0
- package/dist/chunk-OP4DYXYI.js +2 -0
- package/dist/chunk-PNLU5U3P.js +1 -0
- package/dist/chunk-PS7S74DG.js +6 -0
- package/dist/chunk-RMF4YGD6.js +98 -0
- package/dist/chunk-T2GD7RHS.js +10 -0
- package/dist/chunk-TLRM3P7V.js +15 -0
- package/dist/chunk-W3JCQHHU.js +1 -0
- package/dist/chunk-WOVS3GV5.js +10 -0
- package/dist/chunk-YHJZMJYZ.js +1 -0
- package/dist/chunk-ZECBN24L.js +15 -0
- package/dist/chunk-ZEXVHB22.js +2 -0
- package/dist/chunk-ZFCUH5PK.js +7 -0
- package/dist/configure-F7UZCJUG.js +8 -0
- package/dist/create-2E5HVWYK.js +22 -0
- package/dist/db-HEQVTDTM.js +9 -0
- package/dist/delete-XCSIQIRS.js +3 -0
- package/dist/deploy-3QOZEZHC.js +1 -0
- package/dist/dev-JL3ZP5BT.js +86 -0
- package/dist/email-RLZFUFUN.js +10 -0
- package/dist/events-2V5VRBWP.js +15 -0
- package/dist/groups-HDG5DVFY.js +8 -0
- package/dist/index.js +97 -1
- package/dist/install-Z5FYXUYM.js +1 -0
- package/dist/login-KHJBC64Z.js +1 -0
- package/dist/mcp-EFDUKJ5V.js +1 -0
- package/dist/provider-EOI5PTFH.js +13 -0
- package/dist/release-74J5ZIL3.js +1 -0
- package/dist/restore-FFGYV7NO.js +3 -0
- package/dist/secret-7FKKTLHB.js +1 -0
- package/dist/setup-PAKRCAFO.js +1 -0
- package/dist/signin-AP3D4XM4.js +12 -0
- package/dist/status-RMWNHOEK.js +4 -0
- package/dist/teardown-E4JNKAZJ.js +22 -0
- package/dist/update-DWVZFNPS.js +1 -0
- package/dist/workspace-CJC5I66L.js +21 -0
- package/package.json +19 -16
- package/skills/build-every-app/SKILL.md +199 -0
- package/skills/build-every-app/patterns.md +140 -0
- package/skills/everyapp/SKILL.md +292 -0
- package/skills/everyapp/mcp.md +350 -0
- package/skills/everyapp/security-review.md +61 -0
- package/skills/everyapp/setup/README.md +274 -0
- package/skills/everyapp/setup/access.md +178 -0
- package/skills/everyapp/setup/context.md +117 -0
- package/skills/everyapp/setup/dataforseo.md +287 -0
- package/skills/everyapp/setup/email.md +366 -0
- package/skills/everyapp/setup/gateway.md +243 -0
- package/skills/everyapp/setup/google.md +360 -0
- package/skills/everyapp/setup/microsoft-365.md +281 -0
- package/skills/everyapp/setup/openai.md +318 -0
- package/skills/everyapp/setup/openrouter.md +309 -0
- package/skills/everyapp/setup/providers.md +1253 -0
- package/skills/everyapp/setup/remote-mcp.md +199 -0
- package/skills/everyapp/setup/signin-apple.md +178 -0
- package/skills/everyapp/setup/signin-cloudflare.md +148 -0
- package/skills/everyapp/setup/signin-github.md +139 -0
- package/skills/everyapp/setup/signin-google.md +178 -0
- package/skills/everyapp/setup/signin-microsoft.md +183 -0
- package/skills/everyapp/setup/signin-oidc.md +163 -0
- package/skills/everyapp/setup/slack.md +344 -0
- package/skills/everyapp/setup/workers-ai.md +138 -0
- package/bin/__everyapp_bash_complete.js +0 -12
- package/dist/bash-complete.js +0 -3
- package/dist/chunk-7BOQJFI5.js +0 -8
- package/dist/chunk-7YVJFS4A.js +0 -1
- package/dist/chunk-BIXKPCDH.js +0 -2
- package/dist/chunk-DLGPLHBH.js +0 -47
- package/dist/chunk-FD6K2YA7.js +0 -9
- package/dist/chunk-GAJEQYJH.js +0 -39
- package/dist/chunk-O2KDOP5R.js +0 -13
- package/dist/chunk-OI2MBGBG.js +0 -4
- package/dist/chunk-POSDLD3G.js +0 -8
- package/dist/chunk-RB5D2R45.js +0 -1
- package/dist/chunk-U4LIQ5VR.js +0 -1
- package/dist/chunk-VEYSNPDH.js +0 -3
- package/dist/chunk-VP6SKW44.js +0 -7
- package/dist/chunk-WO3NE7ZU.js +0 -7
- package/dist/chunk-XTO2VTPY.js +0 -6
- package/dist/chunk-YJ2DKIZS.js +0 -1
- package/dist/create-DYQ3NBWJ.js +0 -22
- package/dist/deploy-PYTCMDGV.js +0 -7
- package/dist/deploy-RMAHOA4Z.js +0 -55
- package/dist/dev-SAE237FS.js +0 -0
- package/dist/generateConfig-46Z3ERF3.js +0 -1
- package/dist/login-B6EF3SMX.js +0 -1
- package/dist/logout-XMKZBLNV.js +0 -1
- package/dist/remoteD1Shell-A67JMO3H.js +0 -6
- package/dist/setupLocal-TZJUW5ES.js +0 -6
- 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
|