Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export default extendConfig(
items: [
{ text: 'Remote Development', link: '/guide/remote-dev' },
{ text: 'Deploying Your App', link: '/guide/deployment' },
{ text: 'Project Collaboration', link: '/guide/project-collaboration' },
],
},
{
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Already have a Cloudflare Worker and a root `wrangler.jsonc` or `wrangler.json`?

To connect to your team's platform and sign in, run `void connect <url>` using the URL from your administrator. Use `void connect --platform cloudflare` to set up your own Cloudflare account. Connecting preserves existing project links. New Void projects need a platform connection.

Owners can [share a platform project](./project-collaboration.md) with readers, collaborators, and project administrators. This does not apply to direct Cloudflare deployments.

### Migrations

If your app uses Drizzle, `void deploy` runs migrations as part of the deploy flow:
Expand Down
13 changes: 13 additions & 0 deletions docs/guide/platform-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,19 @@ void platform project show <project-id>

Project details include resources, domains, and recent builds and deployments. The [command reference](../reference/cli.md#operator-commands) also covers suspending and restoring users, deleting projects, and removing accounts.

### Transferring Project Ownership

Only an installation administrator can change a project's owner. The new owner must already have an account on this platform. Preview the transfer before applying it:

```sh
void platform project owner <project-id> <new-owner-user-id> --plan
void platform project owner <project-id> <new-owner-user-id> --yes
```

The preview shows both owners, their plans and suspension state, active work that blocks the transfer, and the changes to routing and usage counters. Wait for or cancel any active deploy, rollback, or build before retrying.

After the transfer, the former owner becomes a project administrator. Existing project-scoped CI deploy credentials are revoked; create replacements as the new owner. The new owner's plan and limits apply immediately. Usage before the transfer remains charged to the former owner; later usage is charged to the new owner. If an apply reports partial convergence, inspect the project and operator event log before repeating it.

New users on a self-hosted installation start with the `custom` profile, which
does not cap application requests, AI usage, deployment frequency, or retained
Worker deployments. Named profiles such as `pro` apply the platform's quota and
Expand Down
77 changes: 77 additions & 0 deletions docs/guide/project-collaboration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
outline: deep
---

# Project Collaboration

A project hosted on a Void platform can have one owner and additional readers, collaborators, and project administrators. Team access is a platform feature; it is not available for projects deployed directly to Cloudflare.

## Roles

| Role | Access |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Reader | View the project, usage, resources, deployments, builds, logs, and member roster. Cannot deploy. |
| Collaborator | Reader access plus deploy, rollback, cancellation, migrations, secrets, database configuration, and deploy prerequisites. |
| Project administrator | Collaborator access plus domains, email destinations, GitHub configuration, invitations, roles, and member removal. |
| Owner | Project administrator access plus project deletion. |

Only the owner can create, list, renew, or revoke the project's CI deploy
credentials. Those credentials run unattended deployments as the billing owner
and remain independent of a member's login session, so collaborators and
project administrators deploy with their own account instead of minting a
durable owner credential.

The owner's plan, limits, and suspension state govern the project. A member's
own plan does not change the project's available usage, and acting through a
collaborator does not bypass an owner suspension. A project administrator is
not an installation administrator and receives no platform-wide access.

## Invite a Registered User

An owner or project administrator can invite someone by the email address registered to their existing account on the same platform:

```sh
void project team invite teammate@example.com --role collaborator
```

Pass `--project <slug>` to manage a project other than the linked project. An unknown email is rejected. A project invitation neither creates an account nor adds the address to a restricted-signup allowlist.

Invitations expire after seven days. If the platform cannot send invitation
email, the invitation still appears when the invited user runs
`void project team pending`.

List the roster and sent invitations with:

```sh
void project team list
void project team invitations
```

## Respond to an Invitation

The invited user can see invitations addressed to their account and respond using the invitation ID:

```sh
void project team pending
void project team accept <invitation-id>
# or
void project team decline <invitation-id>
```

An invitation is bound to the registered account. Forwarding its ID does not let another user accept it.

These three invitation-response commands are account-scoped and do not depend
on the current project's deploy target. They can run from a direct Cloudflare
project directory when it resolves your connected Void platform account.

## Change or Remove Access

Owners and project administrators can change any non-owner member or revoke a pending invitation:

```sh
void project team role <user-id> reader
void project team remove <user-id>
void project team revoke <invitation-id>
```

A non-owner member can leave with `void project team leave`. The owner cannot leave; an installation administrator must [transfer ownership](./platform-administration.md#transferring-project-ownership) first.
33 changes: 31 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,33 @@ Link current directory to an existing hosted Void project by slug, or select int

### `void project list`

List all hosted projects (slug, mode, URL). For a saved Cloudflare target, this displays the current Worker's versions instead because there is no Void project registry.
List all accessible hosted projects (slug, role, type, URL). Shared projects are included and the role column distinguishes them from projects you own. For a saved Cloudflare target, this displays the current Worker's versions instead because there is no Void project registry.

### `void project team`

Manage access to a project hosted on a Void platform:

```sh
void project team list [--project <slug>]
void project team invite <email> --role <reader|collaborator|admin> [--project <slug>]
void project team invitations [--project <slug>]
void project team role <user-id> <reader|collaborator|admin> [--project <slug>]
void project team remove <user-id> [--project <slug>]
void project team revoke <invitation-id> [--project <slug>]
void project team leave [--project <slug>]
void project team pending
void project team accept <invitation-id>
void project team decline <invitation-id>
```

Project-scoped commands use `--project`, then `VOID_PROJECT`, then the linked project. Invitations can target only an email address already registered to a user on that platform; they do not create accounts or grant signup access. Only the invited account can accept or decline its invitation.

Readers can view the project but cannot deploy. Collaborators can deploy and manage deploy prerequisites. Project administrators can additionally manage domains, email destinations, GitHub configuration, and the project team. The owner alone can delete the project. See [Project Collaboration](../guide/project-collaboration.md) for the full role boundaries.

Project-scoped team management commands are not available for projects deployed
directly to Cloudflare. The account-scoped `pending`, `accept`, and `decline`
commands do not depend on the project deploy target and can be run from a direct
Cloudflare project directory that resolves the connected Void platform account.

### `void project token <create|list|renew|revoke>`

Expand Down Expand Up @@ -526,10 +552,13 @@ List projects across the platform, filter them by owner, or inspect one project'
void platform project list [--user <user-id>] [--search <text>] [--page <n>] [--limit <n>]
void platform project show <id>
void platform project delete <id>
void platform project owner <project-id> <user-id>
```

Search matches a project's slug, ID, or owner's login. `show` includes resources, domains, the latest 10 deployments, and the latest 20 builds. `delete` removes the project and its resources.

`owner` transfers a project to another registered user. Preview it with `--plan`; apply it interactively or with `--yes`. The preview reports active-work blockers and the account plan that will apply. The former owner becomes a project administrator, existing project-scoped CI deploy credentials are revoked, and usage already incurred remains with the former owner.

#### Deployments {#operator-deployments}

Find a deployment, inspect its manifest, or request cancellation:
Expand Down Expand Up @@ -786,7 +815,7 @@ Platform migrations only move forward. Void checks compatibility before updating

An upgrade completes after the new Workers pass health checks. If rollout fails, Void attempts to restore the previous Workers. Retrying does not repeat completed migrations.

`platform rollback` restores a compatible earlier runtime without reversing database migrations. Pass its files with `--runtime`. If the installed version is a custom build, also supply that version with `--from-runtime`. Void refuses rollbacks that are incompatible with the current database. A later `upgrade` can move forward again.
`platform rollback` restores a compatible earlier runtime without reversing database migrations. Pass its files with `--runtime`. If the installed version is a custom build, also supply that version with `--from-runtime`. Void refuses targets that are incompatible with the current database or predate installed authentication, sandbox-drain, or ownership-aware usage protocols. A later `upgrade` can move forward again.

Uninstall verifies remote ownership before removing anything. Data resources are retained unless you pass `--purge-data`. Workers, R2, AI Gateway, DNS records, routes, custom domains, adopted resources, external PostgreSQL, and zones are always retained for manual review.

Expand Down
16 changes: 10 additions & 6 deletions packages/platform/src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type PlatformDiscoveryDocument = Readonly<{
platformVersion: string;
proxyUrl: string;
schemaVersion: typeof PLATFORM_DISCOVERY_SCHEMA_VERSION | 2 | 3;
usageProtocolVersion: 0 | 1;
usageProtocolVersion: 0 | 1 | 2;
}>;

function normalizeOrigin(value: string, name: 'apiUrl' | 'proxyUrl'): string {
Expand All @@ -50,7 +50,7 @@ function normalizeOrigin(value: string, name: 'apiUrl' | 'proxyUrl'): string {

export function createPlatformDiscoveryDocument(
input: Omit<PlatformDiscoveryDocument, 'schemaVersion' | 'usageProtocolVersion'> & {
usageProtocolVersion?: 0 | 1;
usageProtocolVersion?: 0 | 1 | 2;
},
): PlatformDiscoveryDocument {
if (!input.displayName.trim()) {
Expand All @@ -76,9 +76,10 @@ export function createPlatformDiscoveryDocument(
if (
input.usageProtocolVersion !== undefined &&
input.usageProtocolVersion !== 0 &&
input.usageProtocolVersion !== 1
input.usageProtocolVersion !== 1 &&
input.usageProtocolVersion !== 2
) {
throw new Error('usageProtocolVersion must be 0 or 1.');
throw new Error('usageProtocolVersion must be 0, 1, or 2.');
}
const authentication = input.authentication
? parseAuthentication(input.authentication)
Expand Down Expand Up @@ -251,12 +252,15 @@ function parseAuthentication(value: unknown): PlatformAuthenticationDiscovery {
};
}

function parseUsageProtocolVersion(value: unknown): 0 | 1 {
function parseUsageProtocolVersion(value: unknown): 0 | 1 | 2 {
if (value === undefined || value === 0) {
return 0;
}
if (value === 1) {
return 1;
}
throw new Error('usageProtocolVersion must be 0 or 1.');
if (value === 2) {
return 2;
}
throw new Error('usageProtocolVersion must be 0, 1, or 2.');
}
13 changes: 8 additions & 5 deletions packages/platform/src/installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY = '__void/platform/instal
export const PLATFORM_SANDBOX_DRAIN_KEY = '__void/platform/sandbox-drain/v1';
export const PLATFORM_R2_OWNERSHIP_MARKER_KEY = '__void/platform/ownership/v1';
export const PLATFORM_RESOURCE_OWNERSHIP_SCHEMA_VERSION = 1 as const;
export const PLATFORM_USAGE_PROTOCOL_VERSION = 1 as const;
export const PLATFORM_USAGE_PROTOCOL_VERSION = 2 as const;

export type InstallationStatus =
| 'draft'
Expand Down Expand Up @@ -106,7 +106,7 @@ export type PlatformInstallationManifest = Readonly<{
steps: ReadonlyArray<InstallationStep>;
updatedAt: string;
/** Last usage protocol proven live across the installed Worker set. */
usageProtocolVersion: 0 | 1;
usageProtocolVersion: 0 | 1 | 2;
}>;

export type PlatformResourceOwnershipMarker = Readonly<{
Expand Down Expand Up @@ -251,7 +251,7 @@ export function createPlatformInstallationManifest(input: {
ownershipNonce: string;
platformVersion: string;
safetyProtocols?: Readonly<{ sandboxDrain: 0 | 1 | 2 }>;
usageProtocolVersion?: 0 | 1;
usageProtocolVersion?: 0 | 1 | 2;
}): PlatformInstallationManifest {
const createdAt = assertTimestamp(input.createdAt, 'createdAt');
if (!/^[a-z0-9][a-z0-9-]{7,79}$/.test(input.id)) {
Expand Down Expand Up @@ -537,14 +537,17 @@ function parseAuthenticationResources(
return resources;
}

function parseUsageProtocolVersion(value: unknown): 0 | 1 {
function parseUsageProtocolVersion(value: unknown): 0 | 1 | 2 {
if (value === undefined || value === 0) {
return 0;
}
if (value === 1) {
return 1;
}
throw new Error('usageProtocolVersion must be 0 or 1.');
if (value === 2) {
return 2;
}
throw new Error('usageProtocolVersion must be 0, 1, or 2.');
}

function parseSafetyProtocols(value: unknown): PlatformInstallationManifest['safetyProtocols'] {
Expand Down
9 changes: 6 additions & 3 deletions packages/platform/src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type PlatformRuntimeManifest = Readonly<{
schemaVersion: 2;
sourceRevision: string | null;
/** Queue payload contract shared by the API consumer and trusted producers. */
usageProtocolVersion: 0 | 1;
usageProtocolVersion: 0 | 1 | 2;
}>;

export type PlatformRuntime = Readonly<{
Expand Down Expand Up @@ -219,14 +219,17 @@ function parseSafetyProtocols(value: unknown): PlatformRuntimeManifest['safetyPr
return { sandboxDrain: protocols.sandboxDrain };
}

function usageProtocolVersion(value: unknown): 0 | 1 {
function usageProtocolVersion(value: unknown): 0 | 1 | 2 {
if (value === undefined || value === 0) {
return 0;
}
if (value === 1) {
return 1;
}
throw new Error('runtime manifest usageProtocolVersion must be 0 or 1');
if (value === 2) {
return 2;
}
throw new Error('runtime manifest usageProtocolVersion must be 0, 1, or 2');
}

export function getPlatformRuntimeDirectory(): string {
Expand Down
1 change: 1 addition & 0 deletions packages/platform/src/worker-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function createPlatformWorkerConfigs(input: {

const api: Record<string, unknown> = {
...common,
version_metadata: { binding: 'VERSION_METADATA' },
workers_dev:
publicEndpoints &&
(!deployment.authentication.accessProtection ||
Expand Down
Loading
Loading