Cloudways is our fifth WordPress hosting provider, and architecturally different from Pressable/WP Engine/Kinsta: it provisions real servers, each hosting multiple apps — the same Server hasMany Site shape SpinupWP uses. That’s why Modules\Cloudways\CloudwaysServiceProvider is the only module in this codebase implementing both the HostingProvider and CloudProvider contracts from one class pair — Cloudways is simultaneously “a place sites live with SSH access” and “a fleet of cloud instances to poll metrics from and reconcile for deletion.” See Concepts → Server, Site, Care plan, Hosting tier for where Cloudways sits relative to the other four hosting providers and the three single-purpose cloud providers.
Cloudways provisions on top of DigitalOcean, AWS, GCP, Vultr, or Linode, per the customer’s choice at server-creation time — but this app never holds credentials for whichever of those a given Cloudways server actually runs on. All metrics and alive/dead state for Cloudways servers come from Cloudways’ own API, tracked under servers.provider = 'cloudways', never the underlying cloud’s real identity.
[!TIP] Looking for Testers! Have a live Cloudways account? You can help test this integration! Follow our Contributing & Module Testing Guide to plug in credentials, verify endpoints, vibe code any fixes with Claude, and open a PR back.
Honesty about what’s confirmed here
This module has not been exercised against a live Cloudways account, and carries more open questions than the other three new modules combined:
- API version: Cloudways’ v1 API has reached end-of-life; this module targets v2 exclusively. The specific request/response shapes below are modeled on Cloudways’ documented API specifications.
- The SSH/sudo model is the largest structural risk in this module. Unlike WP Engine/Kinsta (which needed brand-new per-site SSH transports built from scratch), Cloudways sites do have a real
serversrow, soCloudwaysHostingProviderreuses the existingApp\Services\Ssh\SshCommandRunnerandApp\Services\Companion\CompanionInstallerdirectly — no new transport code. But those existing classes assume SpinupWP’s model: one root/sudo-capable SSH login per server, thensudo -u {site_user}to drop privileges per site. Cloudways’ actual SSH model is a per-application “Master Credentials” user (commonlymaster_xxxxx), and whether that user cansudo -u {other_site_user}to reach a sibling app on the same server — or is sandboxed per-app with no cross-app sudo at all — is unknown without testing against a live account. If Cloudways sandboxes per-app SSH (the more likely shape, given app isolation is one of its selling points), the existingCompanionInstallerwill fail for any Cloudways site whosesite_userdoesn’t match the SSH login’s own app, and a Cloudways-specific installer (or a per-app credential model instead of one sharedServer::ssh_password) would be needed instead. - The monitoring/metrics endpoint shape is entirely guessed —
serverMonitorSummaryand/server/diskUsagehave no confirmed v2 shape;CloudwaysMetricsParseris defensive about missing/malformed keys specifically because of this. - Several other endpoints (
/server/{id},/app/{id}, backup/restore, SSL) carry the same “shape assumption” flag inCloudwaysClient’s docblocks.
Populating Site::site_user and Server::ssh_password correctly for Cloudways sites, and confirming the sudo/isolation model, are prerequisites this module assumes but does not itself solve. Do not point this module at a production Cloudways account without testing the SSH path against a real server first.
Setup
-
Generate an API key in the Cloudways platform under Account → API Keys.
-
Set in
.env:CLOCKWORK_CLOUDWAYS_API_KEY=... CLOCKWORK_CLOUDWAYS_EMAIL=... # Safe-by-default: Set to false only when you are ready to enable writes and Companion deployment CLOCKWORK_CLOUDWAYS_VIEW_ONLY=true -
Test:
php artisan clockwork:cloudways-test
# Or using the standardized alias:
php artisan clockwork:test-cloudways
Displays operating mode (Full Access or View Only) and reports total servers and apps.
View-Only (Read-Only) Mode
Cloudways defaults to View-Only Mode (CLOCKWORK_CLOUDWAYS_VIEW_ONLY=true) to safeguard unverified accounts:
- Zero API Mutations: Any call to
POST,PUT, orDELETEthrows aCloudwaysReadOnlyExceptionbefore sending HTTP requests (OAuth access token exchanges are permitted so read calls succeed). - Zero Remote Writes:
commandRunner()andcompanionInstaller()returnnull, disabling SSH command execution and Companion mu-plugin deployment. - Gated Capabilities:
CAP_SSHandCAP_COMPANIONreportfalse. Server and site inventory linkage and cloud metrics collection continue operating normally. - Diagnostic Reporting: Diagnostics at
/settings/diagnosticsappend· Mode: View Only.
To enable write actions and Companion deployment on verified accounts, set:
CLOCKWORK_CLOUDWAYS_VIEW_ONLY=false
Auth
OAuth2-shaped but Cloudways-specific: POST /oauth/access_token with {email, api_key} (form-encoded) returns a short-lived bearer access_token (~24h validity per Cloudways’ docs). CloudwaysClient::token() caches it in-memory for the instance’s lifetime and refreshes on expiry — mirrors AzureClient::token()‘s cache-until-expiry pattern, adapted to Cloudways’ simpler two-field credential exchange.
SSH/command execution goes through the existing App\Services\Ssh\SshCommandRunner and App\Services\Companion\CompanionInstaller — the same classes SpinupWP uses — not a new transport, per the SSH-model caveat above.
Endpoints we call
Base URL https://api.cloudways.com/api/v2.
| Method | Path | Purpose | Confidence |
|---|---|---|---|
| POST | /oauth/access_token |
Token exchange. | Documented v1 shape, unconfirmed for v2. |
| GET | /server |
All servers + their nested apps. CloudwaysCheck’s probe. Also feeds aliveProviderIds()/instancesByIp(). |
Shape assumption. |
| GET | /server/{id} |
Single server detail. | Shape assumption. |
| GET | /app/{id}?server_id={id} |
Single app detail. | Shape assumption. |
| POST | /app/manage/takeBackup |
Trigger an on-demand backup. Async — returns an operation_id. Not wired into any report command yet. |
Shape assumption. |
| POST | /app/manage/restoreBackup |
Restore the most recent backup per component (Cloudways’ v1 docs don’t expose picking an arbitrary historical backup). | Shape assumption. |
| GET | /ssl_certificate/{appId}?server_id={id} |
Cert state for an app. | Shape assumption. |
| POST | /ssl_certificate/install/letsEncrypt |
Install a Let’s Encrypt cert. | Shape assumption. |
| GET | /server/monitorSummary |
Time-series CPU/RAM/load — one call per metric type (type=cpu|ram|load). Feeds CloudwaysCloudProvider::metrics(). |
Guessed — no confirmed v2 shape. |
| GET | /server/diskUsage |
Current disk usage snapshot (used_mb/total_mb). |
Guessed — no confirmed v2 shape. |
CloudwaysCloudProvider::metrics() fetches all four independently with a try/catch per metric — one failing monitor call returns null for that value without failing the other three.
Files
modules/Cloudways/src/CloudwaysClient.php— the REST client (OAuth2 token exchange + caching, servers/apps/backups/SSL/monitoring).modules/Cloudways/src/CloudwaysReadOnlyException.php— thrown when mutating operations are attempted in View-Only mode.modules/Cloudways/src/Commands/CloudwaysTest.php(app/Console/Commands/CloudwaysTest.php) — verification CLI command (clockwork:cloudways-test/clockwork:test-cloudways).modules/Cloudways/src/CloudwaysHostingProvider.php— theHostingProviderhalf. ReusesApp\Services\Ssh\SshCommandRunnerandApp\Services\Companion\CompanionInstaller— see the SSH/sudo-model caveat above before trusting this in production.panelUrl()returnsnull(no confirmed Cloudways Platform deep-link pattern).modules/Cloudways/src/CloudwaysCloudProvider.php— theCloudProviderhalf:id()returnsServer::PROVIDER_CLOUDWAYS,sizeTier()returns the raw slug unchanged (no confirmed Cloudways-specific tier naming — it labels servers by whichever underlying cloud’s own instance sizes were chosen at provisioning),isDeletedAtProvider()follows the DO/Hetzner alive-ID-membership pattern (Cloudways servers are ephemeral/deletable, unlike Azure’s stable resource-ID paths).modules/Cloudways/src/CloudwaysMetricsParser.php— parses the guessed monitoring/disk-usage payload shapes into the scalar valuesmetrics()needs.modules/Cloudways/src/CloudwaysCheck.php— diagnostics probe (OAuth2 exchange +GET /server).modules/Cloudways/src/CloudwaysServiceProvider.php— registers the client binding, manifest, bothhostingProvider()andcloudProvider(), and the diagnostics check.- Config:
config/clockwork.php→cloudwayskey. - Site identifier:
sites.cloudways_app_id(unique, nullable); server identity:servers.provider = 'cloudways'— see the2026_09_03_014554_add_wpengine_kinsta_cloudways_fields_to_sitesmigration andApp\Models\Server::PROVIDER_CLOUDWAYS.
