Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/email/sending.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Check both failure shapes: `result.error` reports a request failure, while `resu

## Setup {#setup}

On a platform with email enabled, deploy without additional email configuration. Ask your administrator for its shared mail domain. Administrators [enable email during installation or upgrade](/guide/platform/installation/credentials#runtime-token-permissions).
On a platform with email enabled, deploy without additional email configuration. Ask your administrator for its shared mail domain. Administrators [enable email during installation or upgrade](/guide/platform/installation/credentials#enable-email).

Each project on an email-enabled platform has:

Expand Down
8 changes: 7 additions & 1 deletion docs/guide/platform/administration/email.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ reconciliation verifies delivery afterward; a deleting project stays closed
and its deletion can be retried. See [operator email commands](../../../reference/cli/platform-email.md#operator-email)
for the recovery requirements.

### Mail that the platform sends {#platform-sender}

The platform sends its own mail, for example invitations, welcome mail, failed-deploy notices, and error alerts. This mail comes from `noreply@<mail domain>`, and the sender name is the platform's display name. Void sets this sender, so you do not need to configure one. Do not set a different sender on the platform Workers in the Cloudflare dashboard. That change stops the next `upgrade`, `repair`, `enable`, or `rollback` until you let Void replace it. See [Platform Workers changed outside Void](/guide/platform/installation/maintenance#workers-changed-outside-void).

Mail to an address that is not verified needs Cloudflare Email Sending onboarding and [Workers Paid](https://dash.cloudflare.com/?to=/:account/workers/plans).

### Deciding who mail may reach

By default, mail from the shared sender reaches only the recipients each project has verified. For a team whose applications mainly mail colleagues, widen that once for the whole installation:
Expand Down Expand Up @@ -114,7 +120,7 @@ Use the apex of the selected Cloudflare zone. Platform-managed custom inbound em
void platform email domain-add other.example --project hr-portal --token-stdin --yes < token.txt
```

Project owners see administrator-registered domains in `void email domain list` and `status`; `status` names the `void platform email` command for any step they cannot take themselves, and `sync` and `remove` point them at `domain-sync` and `domain-remove`. A domain an owner registered before you switched to `admin` stays theirs to renew, sync and remove. The runtime token needs the email permissions listed in the [self-hosting guide](/guide/platform/installation/credentials#runtime-token-permissions) for this to work.
Project owners see administrator-registered domains in `void email domain list` and `status`; `status` names the `void platform email` command for any step they cannot take themselves, and `sync` and `remove` point them at `domain-sync` and `domain-remove`. A domain an owner registered before you switched to `admin` stays theirs to renew, sync and remove. The runtime token needs the [email permissions](/guide/platform/installation/credentials#enable-email) for this to work.

A permission refusal leaves setup blocked. After correcting it, run `domain-sync`
to resume that same setup operation; repeat `domain-rotate-secret` to resume a
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/platform/installation/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ void platform install \

Mutations require `--yes` in CI; a read-only `--plan` does not. Custom runtimes also require `--runtime <directory>`. For a new installation, run `--plan` with the same account, name, and domain options as the unattended install. If you use `--auth-config`, pass the same file to both commands. Register `<platform-api-url>/auth/callback` when creating a login provider's OAuth or OIDC client manually; automatic Access setup manages its own application. A custom API hostname is optional.

In CI, `upgrade`, `repair`, `enable`, `rollback`, and `install --resume` stop before they change a platform Worker or pause traffic when a platform Worker was changed in the Cloudflare dashboard or with the Cloudflare API, or has an active version without a Void deployment record. See [Platform Workers changed outside Void](/guide/platform/installation/maintenance#workers-changed-outside-void).

:::

## Customize the Platform
Expand Down
28 changes: 23 additions & 5 deletions docs/guide/platform/installation/credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,39 @@ Void checks access before provisioning. If it reports a missing permission, upda

## Enable Email {#enable-email}

Choose a shared sender domain and its Cloudflare zone ID. The mail zone may differ from the application zone, but it must belong to the platform's Cloudflare account. To enable email on an existing installation, run:
Choose a shared sender domain and its Cloudflare zone ID. The mail zone may differ from the application zone, but it must belong to the platform's Cloudflare account.

Before you enable email, enable [Cloudflare Email Routing](https://developers.cloudflare.com/email-service/get-started/route-emails/) for the exact shared sender domain and confirm that its MX records point to Cloudflare. For a subdomain, add that name under the zone's **Email Routing → Settings → Subdomains**. Cloudflare adds the required MX and SPF records. The installer's shared-mail bootstrap verifies those records; it does not add them. Keep existing mail-provider records on other domain names in place.

Enabling email lets administrators [register email domains for projects](/guide/platform/administration/email#registering-email-domains-for-projects) and lets projects register destination addresses through the runtime token. The runtime token needs these permissions:

| Scope | Permission | Access |
| ------- | ----------------------- | ------ |
| Account | Email Routing Addresses | Edit |
| Account | Email Sending | Edit |
| Zone | Zone | Read |
| Zone | Zone Settings | Edit |
| Zone | Email Routing Rules | Edit |

The zone permissions must include each zone that will carry mail, including the mail zone. Email Sending onboarding for arbitrary recipients needs Workers Paid.

For a new installation, set `VOID_EMAIL_SENDER_DOMAIN` and `VOID_EMAIL_SHARED_ZONE_ID` before `void platform install`. The runtime-token link then preselects these permissions.

For an existing installation, add the permissions to its runtime token first. Edit the token in the Cloudflare dashboard. The token value stays the same. Or set `VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN` to a new token that has all runtime permissions. Then run:

```sh
VOID_EMAIL_SENDER_DOMAIN=mail.example.com \
VOID_EMAIL_SHARED_ZONE_ID=your-zone-id \
void platform upgrade your-installation-id
```

Set the same two variables before `void platform install` to enable email during a new installation. Void records the pair for later upgrades; an ordinary upgrade cannot replace it.
Void records the domain and zone for later upgrades; an ordinary upgrade cannot replace them.

Before installing, enable [Cloudflare Email Routing](https://developers.cloudflare.com/email-service/get-started/route-emails/) for the exact shared sender domain and confirm that its MX records point to Cloudflare. For a subdomain, add that name under the zone's **Email Routing → Settings → Subdomains**. Cloudflare adds the required MX and SPF records. The installer's shared-mail bootstrap verifies those records; it does not add them. Keep existing mail-provider records on other domain names in place.
Before the CLI changes anything or pauses traffic, it checks that the runtime token can read the mail zone and its Email Routing settings, rules, and addresses. If Cloudflare denies a request, the command stops and names each missing permission. Void keeps a saved runtime token, so you can add the permission to the same token and run the command again. The check cannot confirm Edit access or Email Sending, so select those permissions as the table shows.

Enabling email lets administrators [register email domains for projects](/guide/platform/administration/email#registering-email-domains-for-projects) and lets projects register destination addresses through the runtime token. That needs **Email Routing Addresses: Edit** and **Email Sending: Edit** on the account, plus **Zone: Read**, **Zone Settings: Edit** and **Email Routing Rules: Edit** on the zones that will carry mail. The runtime-token link preselects them when email is enabled. Email Sending onboarding for arbitrary recipients needs Workers Paid.
Without `VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN`, `upgrade`, `repair`, `enable`, and `rollback` check the runtime token saved on this machine. The platform can use a newer token, for example one set from another machine. If Cloudflare does not accept the saved token, the CLI shows a warning and continues without the check. To check the token that the platform uses, set `VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN` to it.

The installer deploys the email gateway, prepares the shared mail route, and verifies inbound readiness before opening platform traffic. If setup fails, correct the reported permission, exact-domain MX records, or routing conflict, then rerun the same install or upgrade command. A fresh install resumes with `void platform install --resume --name <installation-id>`; it continues the recorded email operation after the configuration is corrected.
The installer deploys the email gateway, prepares the shared mail route, and verifies inbound readiness before opening platform traffic. If setup fails, fix the problem that the error names, for example a missing permission, the exact-domain MX records, or a routing conflict. Then run the same install or upgrade command again. A fresh install resumes with `void platform install --resume --name <installation-id>`; it continues the recorded email operation after the configuration is corrected.

## R2 Upload Credentials

Expand Down
34 changes: 32 additions & 2 deletions docs/guide/platform/installation/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,13 @@ An upgrade validates the runtime, applies supported pending migrations, deploys

Before starting, Void checks access to the installation's Cloudflare account, Workers, and existing R2 buckets. If credentials are rejected, set `CLOUDFLARE_API_TOKEN` to a management token scoped to that account and retry. This overrides the installation's saved management token.

::: details Migration safety, credential rotation, and interrupted upgrades
Some upgrades pause application traffic during the cutover to the new version. Traffic resumes after the new version passes its health checks. If an upgrade or another lifecycle command fails after it paused traffic, traffic stays paused until a later run succeeds. To resume:

Some upgrades briefly put the platform into maintenance mode. Traffic resumes after the new version passes health checks. If an upgrade is interrupted during maintenance, rerun the same command to complete it.
1. Run `void platform status <id>` to see the saved progress.
2. Fix the problem that the error names.
3. Run the resume command that the CLI printed. It is the same command, with the same runtime.

::: details Migration safety, credential rotation, and rollback

To update the runtime Cloudflare token, GitHub OAuth credentials when GitHub login is enabled, or R2 credentials,
provide their `VOID_PLATFORM_*` environment variables when running the upgrade.
Expand All @@ -141,6 +145,32 @@ Let running rotations finish before you roll back. If an older proxy runs next t

:::

### Platform Workers changed outside Void {#workers-changed-outside-void}

Void manages the platform's own Workers. Do not change them in the Cloudflare dashboard or with the Cloudflare API. To change the platform, use the Void commands on this page.

`upgrade`, `repair`, `enable`, `rollback`, and `install --resume` deploy the platform Workers again. Before they change a platform Worker or pause traffic, Void finds each platform Worker that:

- was changed in the Cloudflare dashboard or with the Cloudflare API.
- has an active version without a Void deployment record. This can occur when an earlier run replaced a change made outside Void, and then stopped or failed. If that run stopped during the cutover to the new version, the command first makes the earlier versions active again. Void checks those earlier versions too. A version that an older Void release deployed can also have no record.

The `--plan` preview of `upgrade`, `repair`, `enable`, and `rollback` shows the same list as a warning.

In a terminal, Void lists these Workers and asks **Replace the configuration of these Workers?** The default answer is No.

- If you answer No, the command stops. No platform Worker changed, and the command did not pause traffic.
- If you answer Yes, the command continues. First, Void prepares the platform resources and the database. Then, before it pauses traffic or deploys a Worker, the Cloudflare CLI asks you again for each Worker changed in the dashboard or with the API. For a dashboard change, it shows the differences. Void uploads a new version of each of these Workers and does not deploy it yet. For a Worker that has an active version without a Void deployment record, the Cloudflare CLI does not ask again.

If you refuse in the Cloudflare CLI, the command stops. Void does not undo the resource and database changes. No deployment changed, and the command did not pause traffic.

The replacement removes values that Void does not set, for example an `EMAIL_FROM` variable that you added. It also resets values that Void sets, for example `AUTH_REDIRECT_URI`.

Without a terminal, for example in CI, or with `--yes`, the command stops before it changes a platform Worker, and it does not pause traffic. Run the command that Void prints once in a terminal, and answer the questions there. After that run completes, the command works in CI again.

If Void cannot read a platform Worker, the command also stops before it changes a platform Worker or pauses traffic. Run the command again. If a platform Worker has no single version that gets 100% of traffic, for example after a gradual deployment, deploy one version of it to 100% of traffic in the Cloudflare dashboard. Then run the command once in a terminal, so that you can approve the dashboard change.

If an earlier run failed after it paused traffic, traffic stays paused after these stops. To resume traffic, see [Upgrade the platform](#upgrade-the-platform).

### Follow upgrade and rollback progress

Watching progress is optional: a running upgrade or rollback proceeds through
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/platform/installation/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,6 @@ You can also rerun `void platform install` and choose **Continue setup** for an

Resume restores your selected Hyperdrive. If that selection is missing from the saved checkpoint, add `--hyperdrive <existing-id>` when resuming, using the same configuration you originally selected.

If Cloudflare rejects a saved runtime token during continued credential setup, Void opens the token page and asks for a replacement in the same run. Network or service failures do not discard saved tokens. Tokens supplied through the environment must be corrected there instead.
If Cloudflare rejects a saved runtime token during continued credential setup, Void opens the token page and asks for a replacement in the same run. A missing [email permission](/guide/platform/installation/credentials#enable-email) does not discard the saved token: add the permission to the same token, then resume. Network or service failures do not discard saved tokens. Tokens supplied through the environment must be corrected there instead.

:::
2 changes: 2 additions & 0 deletions docs/reference/cli/platform-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ After discovery on another machine, set `VOID_PLATFORM_DATABASE_URL`. See [Manag

Repair and upgrade preserve disabled state. New, resumed, and previously disabled installations block user traffic until all target Workers pass verification; the installer's health probes can still run. Routes and custom domains remain attached.

If a platform Worker was changed outside Void, or has an active version without a Void deployment record, a command that deploys the platform Workers stops before it changes a platform Worker or pauses traffic, unless you agree in a terminal to replace the changes. See [Platform Workers changed outside Void](../../guide/platform/installation/maintenance.md#workers-changed-outside-void).

`--runtime` selects a custom platform build. Relative paths resolve from your current directory. Void verifies the build before making changes; see [Platform Development](../../guide/platform/development/runtime.md#deploying-your-runtime) for creating one.

Without `--runtime`, the CLI uses its packaged platform version.
Expand Down
5 changes: 5 additions & 0 deletions packages/deploy-cloudflare/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export type CloudflareOperation =
assetsDirectory?: string;
previewAlias?: string;
noBundle?: boolean;
/**
* Attach the Cloudflare CLI to this terminal so that it can ask the
* operator. The result then has only the exit status.
*/
interactive?: boolean;
} & WorkerTarget)
| ({ type: 'triggers.deploy'; assets?: boolean } & WorkerTarget)
| ({ type: 'triggers.clear-crons' | 'triggers.clear-domains' } & WorkerTarget)
Expand Down
19 changes: 15 additions & 4 deletions packages/void/src/cli/cloudflare-operation-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function runCf(
command: Array<string>,
cwd = process.cwd(),
bounds?: { timeoutMs: number; maxOutputBytes: number },
interactive = false,
): Result {
const env = cfEnvironment();
env.CF_QUIET = '1';
Expand All @@ -116,7 +117,8 @@ function runCf(
windowsHide: true,
maxBuffer: bounds?.maxOutputBytes ?? 32 * 1024 * 1024,
...(bounds ? { timeout: bounds.timeoutMs } : {}),
stdio: ['ignore', 'pipe', 'pipe'],
// An interactive command shares this process's terminal, so cf can ask there.
stdio: interactive ? 'inherit' : ['ignore', 'pipe', 'pipe'],
});
const stderr = [
child.stderr || '',
Expand Down Expand Up @@ -1060,9 +1062,12 @@ async function workerChanges(): Promise<Result> {
...(request.secretsFilePath ? ['--secrets-file', request.secretsFilePath] : []),
],
directory,
undefined,
request.interactive === true,
),
);
if (called.status !== 0) {
// cf wrote an interactive upload to the terminal, so only its exit status is known.
if (called.status !== 0 || request.interactive === true) {
return called;
}
// cf renders uploads for the terminal; decode once at the process boundary.
Expand Down Expand Up @@ -1272,9 +1277,15 @@ async function main(): Promise<Result> {
return resources();
}

// Login, tail and an interactive upload write to the terminal, not a result envelope.
const terminalOperation =
operation?.type === 'auth.login' ||
operation?.type === 'tail' ||
((operation?.type === 'versions.upload' || operation?.type === 'worker.deploy') &&
operation.interactive === true);
try {
const output = await main();
if (operation.type !== 'auth.login' && operation.type !== 'tail') {
if (!terminalOperation) {
let data = output.data;
let humanOutput = '';
if (data === undefined && output.stdout.trim()) {
Expand Down Expand Up @@ -1303,7 +1314,7 @@ try {
process.exitCode = output.status;
} catch (error) {
const message = error instanceof Error ? error.message : 'Cloudflare operation failed.';
if (operation?.type !== 'auth.login' && operation?.type !== 'tail') {
if (!terminalOperation) {
process.stdout.write(JSON.stringify({ operationResult: 1, errors: [{ message }] }) + '\n');
}
process.stderr.write(message);
Expand Down
Loading
Loading