[ 'host' => [ Host\ValidateHostInput::class, Host\EstablishSshTrust::class, Host\PrepareBaseSystem::class, Host\ConfigureWireguard::class, Host\InstallProxmoxVe::class, Host\RebootIntoPveKernel::class, Host\ConfigureProxmox::class, Host\CreateAutomationToken::class, Host\VerifyProxmoxApi::class, Host\RegisterHostDns::class, Host\RegisterCapacity::class, Host\CompleteHostOnboarding::class, ], 'customer' => [ Customer\ValidateOrder::class, Customer\ReserveResources::class, Customer\CloneVirtualMachine::class, Customer\ConfigureCloudInit::class, Customer\StartVirtualMachine::class, Customer\WaitForGuestAgent::class, Customer\ConfigureNetwork::class, Customer\DeployApplicationStack::class, Customer\ConfigureNextcloud::class, Customer\CreateCustomerAdmin::class, Customer\ConfigureDnsAndTls::class, Customer\RegisterBackup::class, Customer\RegisterMonitoring::class, Customer\RunAcceptanceChecks::class, Customer\CompleteProvisioning::class, ], ], // The one currency the catalogue is priced in. Plan prices carry no currency // of their own, so a payment in anything else cannot be frozen onto a // contract without the contract and the payment disagreeing forever. 'currency' => env('CLUPILOT_CURRENCY', 'EUR'), /* | The plan catalogue does NOT live here any more. It lives in | plan_families / plan_versions / plan_prices, because the owner has to be | able to create and schedule plans from the console, and because a config | array cannot hold a version's history. | | Nothing falls back to a plans array here. A fallback would resurrect a | plan the owner had just switched off, and would let commerce read one | source while provisioning read another — the split-brain this replaced. | See App\Services\Billing\PlanCatalogue. | | quota_gb/disk_gb/ram_mb/cores drive infrastructure placement and stay | ADMIN-ONLY. Customers compare seats, storage, performance class and | features — never raw vCPU/RAM (see docs/specs/2026-07-25-portal-d-*). */ // Default branding applied when a customer has not set their own (resolved by // Customer::brandingResolved(); NULL customer fields fall back to these). 'branding_defaults' => [ 'display_name' => 'CluPilot Cloud', 'logo_path' => null, // null → CluPilot logo used by the provisioner 'primary_color' => '#f97316', 'accent_color' => '#c2560a', ], /* | Traffic policy. Outbound is what counts: inbound is free at our providers, | and it is egress that Hetzner's 20 TB per server applies to. | | Running out throttles rather than blocks — a slow Nextcloud gets a customer | to buy more traffic, a dead one gets a support ticket and a cancellation. */ 'traffic' => [ 'warn_percent' => [80, 95], 'throttle_kbps' => (int) env('CLUPILOT_TRAFFIC_THROTTLE_KBPS', 2048), // ~2 Mbit/s 'addon' => ['gb' => 1000, 'price_cents' => 500], // Sampling interval of the collector, in minutes. Proxmox counters are // cumulative, so a missed run costs accuracy only if the VM restarts // in between. 'sample_minutes' => 15, ], /* | Prices in this config are NET. The rate is configurable because it follows | the seller's country (20 % in Austria), and every price shown to a | customer has to say which it is — a figure without "netto" or "brutto" is | the single most common billing complaint there is. */ 'tax' => [ 'rate_percent' => (float) env('CLUPILOT_TAX_PERCENT', 20), // Ours. A customer whose VAT ID belongs to another EU country is billed // under reverse charge — see App\Services\Billing\TaxTreatment. 'seller_country' => env('CLUPILOT_TAX_COUNTRY', 'AT'), ], // Extra storage add-on (per unit) and the add-on catalogue (labels in lang/*/billing.php). 'storage_addon' => ['gb' => 100, 'price_cents' => 1000], 'addons' => [ 'extra_backups' => ['price_cents' => 500], 'priority_support' => ['price_cents' => 2900], 'collabora_pro' => ['price_cents' => 1900], ], // Feature bullets moved onto plan_versions.features, so that what a version // promises is frozen with the version that promised it. Labels still live in // lang/*/billing.php → billing.feature.. 'dns' => [ 'provider' => 'hetzner', 'token' => env('HETZNER_DNS_TOKEN', ''), 'zone' => env('CLUPILOT_DNS_ZONE', 'clupilot.com'), ], 'traefik' => [ 'dynamic_path' => env('TRAEFIK_DYNAMIC_PATH', '/etc/traefik/dynamic'), ], 'monitoring' => [ // false: a monitoring outage does NOT block delivering a customer's cloud // (the step retries, then continues degraded with a visible event). // true: monitoring registration must succeed or the run fails. 'required' => (bool) env('MONITORING_REQUIRED', false), 'attempts' => (int) env('MONITORING_ATTEMPTS', 2), ], // CluPilot VM acts as the WireGuard hub; hosts join it during onboarding. 'wireguard' => [ // The hub's own address inside the tunnel — what a client uses as its // resolver when the console is reachable there. 'hub_address' => env('CLUPILOT_WG_HUB_ADDRESS', '10.66.0.1'), 'subnet' => $wgSubnet, 'hub_ip' => env('CLUPILOT_WG_HUB_IP', $wgHubDefault), // handshake target 'endpoint' => env('CLUPILOT_WG_ENDPOINT', ''), // host:port reachable by peers 'hub_public_key' => env('CLUPILOT_WG_HUB_PUBKEY', ''), 'config_path' => env('CLUPILOT_WG_CONFIG_PATH', '/etc/wireguard/wg0.conf'), ], // SSH identity CluPilot deploys to each host after first password login. 'ssh' => [ // Prefer *_PATH (a file on disk); fall back to the inline env value. 'public_key' => $secretFrom('CLUPILOT_SSH_PUBLIC_KEY_PATH', 'CLUPILOT_SSH_PUBLIC_KEY'), 'private_key' => $secretFrom('CLUPILOT_SSH_PRIVATE_KEY_PATH', 'CLUPILOT_SSH_PRIVATE_KEY'), // Per-command SSH timeout; below the job timeout (2100s) but far above // the phpseclib ~10s default so apt full-upgrade etc. can finish. 'command_timeout' => (int) env('CLUPILOT_SSH_COMMAND_TIMEOUT', 2000), ], // Proxmox automation role/user created on each host. 'proxmox' => [ 'role_id' => 'CluPilotAutomation', 'role_privs' => 'VM.Allocate,VM.Clone,VM.Config.Disk,VM.Config.CPU,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.Config.Cloudinit,VM.PowerMgmt,VM.Monitor,VM.Audit,VM.Backup,VM.GuestAgent.Audit,VM.GuestAgent.Unrestricted,Datastore.AllocateSpace,Datastore.Audit,Sys.Audit', 'user' => 'automation@pve', 'token_name' => 'clupilot', ], ];