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/platform/installation/credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Use the following permissions for the core platform. Cloudflare may label write

When Void configures Access in the installation account, the initial management-token link also selects **Account → Access: Apps and Policies → Edit** and **Account → Access: Organizations, Identity Providers, and Groups → Read**. Protection adds **Account → Access: Service Tokens → Edit**. When the authentication configuration or saved setup selects existing applications, the link requests Read access instead of Edit. These permissions belong to the management token; the runtime token does not need them.

The management token also needs **Zone Edit** with authority to create zones if you ask Void to create the zone. If it already exists, use the selected zone with Zone Read and DNS Edit. Nested application domains additionally need **SSL and Certificates: Read** on the management token. A custom runtime that enables custom project domains needs **SSL and Certificates: Edit** on the runtime token; the core runtime does not enable that feature.
The management token also needs **Zone Edit** with authority to create zones if you ask Void to create the zone. If it already exists, use the selected zone with Zone Read and DNS Edit. Nested application domains additionally need **SSL and Certificates: Read** on the management token. An installation with [project custom domains](/guide/platform/installation/domains#project-custom-domains) needs **SSL and Certificates: Edit** on the runtime token.

Void checks access before provisioning. If it reports a missing permission, update the token's permissions for the selected account or zone and retry.

Expand Down
16 changes: 16 additions & 0 deletions docs/guide/platform/installation/domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ The management token needs **SSL and Certificates: Read** (or Edit) on that zone

:::

## Project Custom Domains

Projects can serve their own domains, such as `www.example.com`, through Cloudflare for SaaS custom hostnames. This needs a dedicated application zone. Turn it on when installing, or later with an upgrade:

```sh
void platform install --application-domain example.app --zone example.app --dedicated-zone --custom-domains
void platform upgrade <installation-id> --custom-domains
```

The runtime token needs **SSL and Certificates: Edit** on the application zone. Void includes it in the runtime token's creation link and checks it before deploying; a token created before custom domains were on needs it added. Before adding the first domain, set up Cloudflare for SaaS on that zone once:

1. Create a proxied DNS record for `cname.<application domain>`, for example an `AAAA` record pointing to `100::`.
2. Open **SSL/TLS > Custom Hostnames**, enable Cloudflare for SaaS, and set `cname.<application domain>` as the fallback origin.

A project owner then runs `void domain add <hostname>`. Void prints the CNAME target, `cname.<application domain>`, and the TXT records that prove ownership. Once the records resolve and the certificate is issued, the dedicated zone's catch-all routes send the hostname to the project.

## Optional Dashboard

The API's `/admin/` pages are included in the core installation. You can deploy
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/self-hosted-platform.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ If you're deploying an app for yourself, [deploy directly to Cloudflare](/integr

The core platform supports GitHub, Google, generic OIDC, and Cloudflare Access login, CLI deploys, D1, KV, R2, Queues, cron jobs, Workers AI, WebSockets, SSE, ISR, routing, logs, and rollback.

The core installation includes managed Sandboxes and an email gateway; email is enabled when you configure a shared mail domain and zone. The user dashboard, GitHub builds and webhooks, build Containers, and custom project domains aren't part of the core installation. Source-built platforms that add the optional GitHub services should follow the [isolated webhook ingress setup](/guide/platform/development/runtime#optional-github-webhook-ingress-for-access-protected-apis) when Access protects the API.
The core installation includes managed Sandboxes and an email gateway; email is enabled when you configure a shared mail domain and zone. The user dashboard, GitHub builds and webhooks, and build Containers aren't part of the core installation. [Project custom domains](/guide/platform/installation/domains#project-custom-domains) are opt-in with `--custom-domains`. Source-built platforms that add the optional GitHub services should follow the [isolated webhook ingress setup](/guide/platform/development/runtime#optional-github-webhook-ingress-for-access-protected-apis) when Access protects the API.

## Deployment features

Expand Down
5 changes: 3 additions & 2 deletions docs/reference/cli/platform-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ void platform install [options] [--yes]
| `--zone <domain>` | Cloudflare zone containing the application domain |
| `--dedicated-zone` | Add zone-wide catch-all routes; valid only when the app domain is the whole zone |
| `--no-dedicated-zone` | Skip zone-wide application catch-all routes |
| `--custom-domains` | Let projects add their own domains; needs a dedicated zone |
| `--control-plane-domain <domain>` | Optional API custom hostname; defaults to `workers.dev` |
| `--no-control-plane-domain` | Use the default `workers.dev` API hostname |
| `--dashboard-url <origin>` | HTTPS origin of an optional dashboard deployed separately |
Expand Down Expand Up @@ -100,9 +101,9 @@ Use these commands to recover, update, pause, or remove an installation:

```sh
void platform discover [--account <id>] [--installation <id-or-name>]
void platform upgrade [id] [--runtime <path>] [--dashboard-url <origin>] [--plan] [--yes]
void platform upgrade [id] [--runtime <path>] [--dashboard-url <origin>] [--custom-domains] [--plan] [--yes]
void platform rollback [id] --runtime <earlier-path> [--from-runtime <current-path>] [--plan] [--yes]
void platform repair [id] [--runtime <path>] [--dashboard-url <origin>] [--auth-config <path>] [--plan] [--yes]
void platform repair [id] [--runtime <path>] [--dashboard-url <origin>] [--custom-domains] [--auth-config <path>] [--plan] [--yes]
void platform disable [id] [--plan] [--yes]
void platform enable [id] [--runtime <path>] [--plan] [--yes]
void platform uninstall [id] [--plan] [--purge-data | --purge-resources] [--yes]
Expand Down
10 changes: 9 additions & 1 deletion packages/void/src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,10 @@ const HELP_TREE: HelpTree = {
command: '--no-dedicated-zone',
description: 'Skip zone-wide application catch-all routes',
},
{
command: '--custom-domains',
description: 'Let projects add their own domains (requires --dedicated-zone)',
},
{ command: '--control-plane-domain <domain>', description: 'Optional API hostname' },
{
command: '--no-control-plane-domain',
Expand Down Expand Up @@ -1463,7 +1467,7 @@ function lifecycleHelp(
return {
summary,
usage: [
`void platform ${command} [id] [--plan]${command === 'disable' ? '' : ' [--runtime <path>]'}${command === 'upgrade' || command === 'repair' ? ' [--dashboard-url <origin>]' : ''}${command === 'repair' ? ' [--auth-config <path>]' : ''} [--yes]`,
`void platform ${command} [id] [--plan]${command === 'disable' ? '' : ' [--runtime <path>]'}${command === 'upgrade' || command === 'repair' ? ' [--dashboard-url <origin>] [--custom-domains]' : ''}${command === 'repair' ? ' [--auth-config <path>]' : ''} [--yes]`,
],
options: [
{ command: '--plan', description: 'Print the exact plan without making changes' },
Expand All @@ -1482,6 +1486,10 @@ function lifecycleHelp(
description:
'Configure a separately deployed dashboard; omission keeps its saved origin',
},
{
command: '--custom-domains',
description: 'Turn on project custom domains; omission keeps the saved setting',
},
]
: []),
...(command === 'repair'
Expand Down
22 changes: 21 additions & 1 deletion packages/void/src/cli/platform-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export type PlatformArgs =
hyperdrive?: string;
noControlPlaneDomain?: true;
dedicatedZoneSpecified?: true;
customDomains?: true;
account: string;
applicationDomain: string;
controlPlaneDomain: string;
Expand All @@ -99,6 +100,7 @@ export type PlatformArgs =
/** repair only: replaces the login settings while first administrator setup is pending. */
authConfig?: string;
dashboardUrl?: string;
customDomains?: true;
selector: string;
plan: boolean;
runtime: string;
Expand Down Expand Up @@ -168,12 +170,14 @@ function parseLifecycleSelector(
yes: boolean;
authConfig?: string;
dashboardUrl?: string;
customDomains?: true;
} {
const positional: Array<string> = [];
let runtime = '';
let fromRuntime = '';
let authConfig: string | undefined;
let dashboardUrl: string | undefined;
let customDomains = false;
for (let index = 0; index < rest.length; index++) {
const arg = rest[index]!;
if (arg === '--runtime') {
Expand All @@ -187,6 +191,11 @@ function parseLifecycleSelector(
const [value, next] = readValue(rest, index, arg);
dashboardUrl = normalizePlatformDashboardUrl(value);
index = next;
} else if (
arg === '--custom-domains' &&
(subcommand === 'upgrade' || subcommand === 'repair')
) {
customDomains = true;
} else if (arg === '--auth-config' && subcommand === 'repair') {
const [value, next] = readValue(rest, index, arg);
authConfig = value;
Expand All @@ -209,6 +218,7 @@ function parseLifecycleSelector(
fromRuntime,
...(authConfig !== undefined ? { authConfig } : {}),
...(dashboardUrl !== undefined ? { dashboardUrl } : {}),
...(customDomains ? { customDomains: true as const } : {}),
};
}

Expand Down Expand Up @@ -319,7 +329,7 @@ export function parsePlatformArgs(args: Array<string>): PlatformArgs {
subcommand === 'disable' ||
subcommand === 'enable'
) {
const { selector, plan, runtime, fromRuntime, yes, authConfig, dashboardUrl } =
const { selector, plan, runtime, fromRuntime, yes, authConfig, dashboardUrl, customDomains } =
parseLifecycleSelector(rest, subcommand);
return subcommand === 'rollback'
? { subcommand, selector, plan, runtime, fromRuntime, yes }
Expand All @@ -331,6 +341,7 @@ export function parsePlatformArgs(args: Array<string>): PlatformArgs {
yes,
...(authConfig !== undefined ? { authConfig } : {}),
...(dashboardUrl !== undefined ? { dashboardUrl } : {}),
...(customDomains ? { customDomains } : {}),
};
}
if (subcommand === 'uninstall') {
Expand Down Expand Up @@ -373,6 +384,7 @@ export function parsePlatformArgs(args: Array<string>): PlatformArgs {
'--zone',
]);
let workersDev = false;
let customDomains = false;
for (let i = 0; i < rest.length; i++) {
const arg = rest[i]!;
if (valueFlags.has(arg)) {
Expand All @@ -399,6 +411,8 @@ export function parsePlatformArgs(args: Array<string>): PlatformArgs {
yes = true;
} else if (arg === '--workers-dev') {
workersDev = true;
} else if (arg === '--custom-domains') {
customDomains = true;
} else {
throw new Error(`platform: Unexpected argument '${arg}'.`);
}
Expand Down Expand Up @@ -440,10 +454,16 @@ export function parsePlatformArgs(args: Array<string>): PlatformArgs {
'platform: --workers-dev cannot be combined with a domain, zone, or --dedicated-zone.',
);
}
if (customDomains && (workersDev || noDedicatedZone)) {
throw new Error(
'platform: --custom-domains needs a dedicated application zone; it cannot be combined with --workers-dev or --no-dedicated-zone.',
);
}
return {
subcommand,
...(noControlPlaneDomain ? { noControlPlaneDomain: true as const } : {}),
...(noDedicatedZone ? { dedicatedZoneSpecified: true as const } : {}),
...(customDomains ? { customDomains: true as const } : {}),
...(values.has('--hyperdrive') ? { hyperdrive: values.get('--hyperdrive')! } : {}),
...(loginMethods
? { loginMethods: [...new Set(loginMethods)] as Array<PlatformLoginKind> }
Expand Down
5 changes: 4 additions & 1 deletion packages/void/src/cli/platform-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ export async function runPlatformLifecycleCommand(commandArgs: PlatformArgs): Pr
args.accessProtection === undefined &&
args.appAccess === undefined &&
!args.dedicatedZoneSpecified &&
args.dashboardUrl === undefined
args.dashboardUrl === undefined &&
!args.customDomains
? listSavedPlatformInstallPlans().filter(
(saved) =>
(!args.name || saved.args.name === normalizeInstallationSlug(args.name)) &&
Expand Down Expand Up @@ -731,6 +732,7 @@ export async function runPlatformLifecycleCommand(commandArgs: PlatformArgs): Pr
applicationDomain,
controlPlaneDomain,
...(args.dashboardUrl !== undefined ? { dashboardUrl: args.dashboardUrl } : {}),
...(args.customDomains ? { customDomains: true } : {}),
dedicatedZone,
displayName,
...(externalHyperdrive ? { externalHyperdrive } : {}),
Expand Down Expand Up @@ -761,6 +763,7 @@ export async function runPlatformLifecycleCommand(commandArgs: PlatformArgs): Pr
zoneName,
controlPlaneDomain,
...(args.dashboardUrl !== undefined ? { dashboardUrl: args.dashboardUrl } : {}),
...(args.customDomains ? { customDomains: true } : {}),
dedicatedZone,
workersDev,
authConfig: args.authConfig,
Expand Down
4 changes: 4 additions & 0 deletions packages/void/src/cli/platform-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ export function platformLifecycleResumeCommand(
if ('dashboardUrl' in args && args.dashboardUrl) {
command.push('--dashboard-url', commandArgument(args.dashboardUrl));
}
if ('customDomains' in args && args.customDomains) {
command.push('--custom-domains');
}
if (args.yes) {
command.push('--yes');
}
Expand Down Expand Up @@ -427,6 +430,7 @@ export async function runInstalledPlatformCommand(args: PlatformArgs): Promise<v
...(args.subcommand !== 'rollback' && args.dashboardUrl !== undefined
? { dashboardUrl: args.dashboardUrl }
: {}),
...(args.subcommand !== 'rollback' && args.customDomains ? { customDomains: true } : {}),
dedicatedZone: deployment.application.dedicatedZone,
displayName: deployment.displayName,
installationSlug: deployment.installationSlug,
Expand Down
23 changes: 20 additions & 3 deletions packages/void/src/cli/platform-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type PlatformInstallPlanInput = Readonly<{
applicationDomain: string;
controlPlaneDomain: string;
dashboardUrl?: string;
customDomains?: boolean;
dedicatedZone: boolean;
displayName: string;
externalHyperdrive?: PlatformExternalHyperdrive;
Expand Down Expand Up @@ -197,7 +198,14 @@ export async function createPlatformInstallPlan(
}
: {}),
...(enableEmail ? { email: true } : {}),
...(input.customDomains ? { customDomains: true } : {}),
};
// Custom hostnames reach dispatch through the dedicated zone's catch-all routes.
if (capabilities.customDomains && (testing || !input.dedicatedZone)) {
throw new Error(
'platform: Custom domains need a dedicated application zone. Install with a domain and --dedicated-zone.',
);
}
if (capabilities.email && options?.includeEmailGateway !== false && !enableEmail) {
throw new Error(
'platform: Email-enabled installations require VOID_EMAIL_SENDER_DOMAIN and VOID_EMAIL_SHARED_ZONE_ID before deploying this runtime.',
Expand Down Expand Up @@ -269,13 +277,22 @@ export async function createPlatformInstallPlan(
: {}),
},
};
const comparableExistingWithoutEmailZone =
existing && deployment.capabilities.email && !existing.deployment.capabilities.email
const existingWithCustomDomains =
existing &&
deployment.capabilities.customDomains &&
!existing.deployment.capabilities.customDomains
? {
...existingWithDashboard,
capabilities: { ...existingWithDashboard.capabilities, email: true },
capabilities: { ...existingWithDashboard.capabilities, customDomains: true },
}
: existingWithDashboard;
const comparableExistingWithoutEmailZone =
existing && deployment.capabilities.email && !existing.deployment.capabilities.email
? {
...existingWithCustomDomains,
capabilities: { ...existingWithCustomDomains.capabilities, email: true },
}
: existingWithCustomDomains;
if (
existing &&
(JSON.stringify(comparableExistingWithoutEmailZone) !==
Expand Down
4 changes: 4 additions & 0 deletions packages/void/src/cli/platform-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export type PlatformInstallCommandOptions = {
zoneName: string;
controlPlaneDomain: string;
dashboardUrl?: string;
customDomains?: boolean;
dedicatedZone: boolean;
workersDev: boolean;
authConfig?: string;
Expand Down Expand Up @@ -337,6 +338,9 @@ export function platformInstallCommandArguments(
} else {
parts.push('--no-dedicated-zone');
}
if (options.customDomains) {
parts.push('--custom-domains');
}
}
flag('--control-plane-domain', options.controlPlaneDomain);
if (!options.controlPlaneDomain) {
Expand Down
16 changes: 16 additions & 0 deletions packages/void/test/unit/platform-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ describe('platform args', () => {
).toThrow();
}
});
it('turns on custom domains only in install, upgrade, and repair', () => {
for (const command of ['install', 'upgrade', 'repair']) {
expect(parsePlatformArgs([command, '--custom-domains'])).toMatchObject({
customDomains: true,
});
expect(parsePlatformArgs([command])).not.toHaveProperty('customDomains');
}
for (const command of ['enable', 'disable', 'rollback', 'uninstall']) {
expect(() => parsePlatformArgs([command, '--custom-domains'])).toThrow();
}
for (const flag of ['--workers-dev', '--no-dedicated-zone']) {
expect(() => parsePlatformArgs(['install', '--custom-domains', flag])).toThrow(
'dedicated application zone',
);
}
});
it('accepts replacement login settings only in repair', () => {
expect(parsePlatformArgs(['repair', 'team', '--auth-config', 'auth.json'])).toEqual({
subcommand: 'repair',
Expand Down
3 changes: 2 additions & 1 deletion packages/void/test/unit/platform-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ describe('platform credential setup links', () => {
zoneName: 'example.com',
controlPlaneDomain: 'api.example.com',
dedicatedZone: true,
customDomains: true,
workersDev: false,
authConfig: './auth config.json',
dashboardUrl: 'https://dash.example.com:8443',
Expand All @@ -229,7 +230,7 @@ describe('platform credential setup links', () => {
const quotedName =
process.platform === 'win32' ? "'Team''s Platform'" : "'Team'\\''s Platform'";
expect(command).toBe(
`void-dev platform install --name team --account account --display-name ${quotedName} --application-domain apps.example.com --zone example.com --dedicated-zone --control-plane-domain api.example.com --auth-config './auth config.json' --dashboard-url https://dash.example.com:8443 --runtime './build output' --yes`,
`void-dev platform install --name team --account account --display-name ${quotedName} --application-domain apps.example.com --zone example.com --dedicated-zone --custom-domains --control-plane-domain api.example.com --auth-config './auth config.json' --dashboard-url https://dash.example.com:8443 --runtime './build output' --yes`,
);
});

Expand Down
Loading