You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 3d8f848
Browse filesBrowse the repository at this point in the historyBrowse files
feat(email): permanent short alias on the shared mail domain (#553)
Fixes#511
## What this adds
A project owner can give the project one short, permanent name on the
platform's shared mail domain:
```sh
void email alias acme # asks to confirm; --yes skips the prompt
void email alias # shows the address and inbound status
```
```
before after (slug acme-store, alias acme)
────── ─────
acme-store@mail.example.com acme-store@mail.example.com ← still works
acme-store+support@… acme-store+support@… ← still works
acme@mail.example.com ← new
acme+support@… ← new
```
Both names send and receive. Handlers route the same way either way:
`acme+support@…` and `acme-store+support@…` both reach
`email/support.ts`.
## Answers to the three questions
### 1. Alias field, or another way?
A field: `projects.email_alias` (nullable, migration 0062).
```
projects
slug acme-store (unique, as before)
email_alias acme (unique when set)
one namespace: a trigger refuses
alias = any slug slug = any alias alias = own slug
```
- The DB trigger (`void_project_mailbox_name_guard`, SQLSTATE 23505)
keeps slugs and aliases apart in both directions, including on older
runtimes after a rollback.
- The alias follows the slug rules: shape, 56 characters, reserved
names, no `preview-`. Names of five characters or fewer need the same
plan permission as short slugs.
- Only the owner can set it (`PUT /projects/:id/email/alias`).
### 2. Permanent, or changeable?
Permanent. Set once, never changed or removed, and freed only when the
project is deleted.
- Reply addresses people have saved (`acme+ticket-42@…`) keep working.
- No swap logic, so no window where mail bounces.
- Because it is permanent, the route checks Cloudflare **before**
saving: it refuses a name that an existing rule already routes
(including plus addresses), and refuses when the domain has no room for
the rules this project will need.
### 3. Does the old slug keep working?
Yes, for sending and receiving. An alias project uses two Cloudflare
literal rules:
```
Cloudflare Email Routing (shared domain)
mail to acme-store+x@ ─▶ rule: acme-store@ ─┐
mail to acme+x@ ─▶ rule: acme@ ─┴─▶ email gateway ─▶ resolve-ingress
│ finds project by slug OR alias
│ checks readiness of THAT name
▼
proxy router ─▶ project worker
(prefix must equal the name used)
```
- Default `from` stays `<slug>+noreply@…`. Apps that want the alias as
sender set `from`.
- Admission accepts `From` with either name. Replies and forwards can
use either name, whatever address the mail came in on.
- Deploys only wait for the slug rule. A failing alias rule shows in
`void email alias` but never blocks a deploy.
## Rollback
```
upgrade ──▶ alias rules created
rollback ─▶ older runtime ignores email_alias; its reconcile removes alias rules;
slug mail keeps working
upgrade ──▶ next reconcile restores the alias rows and rules
(including a removal that stopped part way)
```
- `compatibility.json` needs no change: no release contains 0061, and
the existing 1.1.11–1.2.1 entries now cover 0062 (covered by a test).
- Known rollback gap, written down in `platform/meta/operations.md`: on
an older runtime, creating a project whose slug equals an existing alias
returns 500 (the trigger refuses it), not `slug already taken`.
## Rule limit
Each receiving alias project uses one more rule out of Cloudflare's
default 200 per domain. The admin docs say so.
## Tests
- Migration: upgrade from the 1.1.11–1.2.1 schema, trigger refusals,
older-runtime inserts.
- Shared recipients: second rule, removed / half-removed row restore,
per-name readiness, cron pickup, room and conflict checks, deploy not
blocked by the alias.
- Route: owner only, validation, plan check, taken, Cloudflare conflict,
settings.
- Ingress, admission, proxy router (mailbox compare, reply pins),
gateway validation, generated `handleEmail` prefix strip.
- CLI: args, help, the command.
Local: typecheck, lint, fmt, `vp run test` (affected) pass. Browser
tests can't run on this machine (TLS proxy blocks the Playwright
download).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/guide/email/receiving.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -94,6 +94,8 @@ deployed `email/` handlers without per-project DNS setup once the platform has v
94
94
handlers once its inbound readiness is `ready`. Use `void email domain status`
95
95
to check current provider routing and any remaining setup steps.
96
96
97
+
A project with an [email alias](../../reference/cli/email.md#void-email-alias) also receives at `<alias>+anything@<mail domain>`. Routing ignores which prefix was used: with the slug `acme-store` and the alias `acme`, both `acme-store+support@…` and `acme+support@…` reach `email/support.ts`.
98
+
97
99
For direct deployments, follow [Your own Cloudflare account](./domains.md#your-own-cloudflare-account) to configure inbound addresses.
98
100
99
101
The local dev server provides the outbound email inbox but has no inbound email trigger. Test inbound handlers with `createInboundTestHarness` below.
Copy file name to clipboardExpand all lines: docs/guide/email/sending.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -84,7 +84,7 @@ You can send to at most 50 recipients across `to`, `cc`, and `bcc`. Use standard
84
84
85
85
`Address` accepts either a string (`"hello@acme.dev"` or `"Name <hello@acme.dev>"`) or an object (`{ email, name? }`). Unicode display names are supported.
86
86
87
-
On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@<mail-domain>` or `<project-slug>+<tag>@<mail-domain>`, optionally with a display name — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](./domains.md#your-own-domain-on-the-platform)). For example, if your platform's mail domain is `mail.example.com`, you can use `Acme <acme+noreply@mail.example.com>`. Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@<mail-domain>` for you.
87
+
On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@<mail-domain>` or `<project-slug>+<tag>@<mail-domain>`, optionally with a display name — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](./domains.md#your-own-domain-on-the-platform)). For example, if your platform's mail domain is `mail.example.com`, you can use `Acme <acme+noreply@mail.example.com>`. Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@<mail-domain>` for you. If the project owner set an [email alias](../../reference/cli/email.md#void-email-alias), `<alias>@<mail-domain>` and `<alias>+<tag>@<mail-domain>` are accepted too; the default stays the slug address.
88
88
89
89
On your own Cloudflare account, `from` defaults to `email.from` from `void.config.ts` and must be on a domain your account can send from; Cloudflare rejects any other sender and `sendEmail` reports it as `INVALID_FROM`.
Copy file name to clipboardExpand all lines: docs/guide/platform/administration/email.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,6 +10,8 @@ These commands apply to a platform that enables email. Every project can send fr
10
10
11
11
Shared inbound mail uses one exact routing rule per receiving project and enables Cloudflare’s per-zone plus addressing. Creating a project or deploying an app that only sends mail consumes no inbound rule. Existing rules and the apex catch-all remain in place. Cloudflare normally allows [200 routing rules per domain](https://developers.cloudflare.com/email-service/platform/limits/); remove unused rules or request a higher limit if the shared domain reaches capacity. An unrelated rule for a project’s base address or a plus address blocks its setup until that conflict is removed.
12
12
13
+
A project owner can give the shared address one permanent, shorter name with `void email alias <name>`. A receiving project with an alias uses two rules, one for the slug and one for the alias, because the slug address keeps working. Setting an alias is refused while a conflicting rule exists or the domain is at its rule limit, and the alias is released only when the project is deleted.
14
+
13
15
If recipient creation is interrupted, inspect its retained intent:
Remove one recipient from the project's destination list. New sends to that address are refused immediately; previously admitted attempts may finish. No deploy is involved.
52
52
53
+
## `void email alias` {#void-email-alias}
54
+
55
+
```
56
+
void email alias [--project <name>]
57
+
void email alias <name> [--yes] [--project <name>]
58
+
```
59
+
60
+
With no name, show the project's address on the platform's shared mail domain and whether inbound mail reaches it.
61
+
62
+
With a name, give that address a shorter name: `void email alias acme` makes `acme@<mail domain>` and `acme+<tag>@<mail domain>` reach the project and lets it send from them. Only the project owner can set it. The name follows the same rules as a project slug, must not be used by any other project's slug or alias, and names of five characters or fewer need a plan that allows short names.
63
+
64
+
The alias is permanent: it cannot be changed or removed, and it is freed only when the project is deleted. The slug address keeps working for sending and receiving, so nothing that already uses it breaks. The CLI asks for confirmation; pass `--yes` to skip the prompt, which is required outside an interactive terminal. Inbound mail at the alias starts once the project has `email/` handlers deployed and its recipient rule is ready; run `void email alias` to check. Redeploy with the current `void` before you share the alias: an app built with an older version sees `message.to` with the alias still in front (`acme+support@…` instead of `support@…`).
'email: `void email setup` sets email up on your OWN Cloudflare account — run it with `--platform cloudflare`. The Void platform needs no setup: deploy, and `void email usage` shows your address.',
53
+
'email: `void email setup` sets email up on your OWN Cloudflare account — run it with `--platform cloudflare`. The Void platform needs no setup: deploy, and `void email alias` shows your address.',
printSuccess(`Email alias set: ${result.address}`);
148
+
conststatus=result.sharedInbound?.status;
149
+
if(status&&status!=='ready'){
150
+
constother=
151
+
result.sharedInbound!.recipient===result.address
152
+
? ''
153
+
: ` at ${result.sharedInbound!.recipient}`;
154
+
log.info(
155
+
`Inbound mail${other} is ${status}${result.sharedInbound?.reason ? `: ${result.sharedInbound.reason}` : ''}. It starts working once the project has an email handler deployed and routing is ready; check with \`void email alias\`.`,
156
+
);
157
+
}
158
+
}
159
+
160
+
/**
161
+
* The inbound status covers every name of the project, so name the address
162
+
* it belongs to when that is not the one shown (the slug row can be the one
0 commit comments