SpinupWP is the WordPress hosting control plane sitting under our fleet. We use its API for inventory bootstrap — initial server + site import, periodic resync, single-site refresh on demand. All runtime traffic (banning, plugin probing, log tailing) goes over SSH directly. This module is verified and in active production use.
Why we use it
A daily SpinupWP import keeps our local view of the fleet honest as the operator adds, removes, or re-tiers sites. SpinupWP also exposes the SSL renewal date, which we feed into the per-site cert state machine. Additionally, SpinupWP integrates with our multi-provider Backup Relay pipeline via SpinupWpBackupRelayAdapter (CAP_BACKUP_RELAY), streaming backups to S3 Glacier Instant Retrieval.
What SpinupWP cannot tell us directly:
- Plugin inventory per site — exposed only as boolean update flags. Our
WpPluginDetectordoes an SSH + wp-cli probe instead. - Backup execution history — only configuration. We query DigitalOcean Spaces directly to track real snapshot runs and archive sizes.
- DB credentials — only
database.table_prefix. We extract the rest fromwp-config.phpover SSH on first onboarding.
Backup Monitoring with DigitalOcean Spaces
SpinupWP writes automated backup archives directly to an S3-compatible DigitalOcean Spaces bucket. While SpinupWP’s API only exposes backup configuration (schedule and retention), Clockwork Control integrates with DigitalOcean Spaces to monitor actual backup execution:
- Site Overview Card: The Backups card on the site overview dashboard detects SpinupWP sites configured with Spaces. If backup archives exist for the site’s domain (
<domain>/<timestamp>-<suffix>), the site displays as Protected with destinationDigitalOcean Spaces (SpinupWP)and the latest backup timestamp. - Snapshot History Modal: An interactive “Snapshots” modal on the card queries
GET /sites/{site}/backups-historyto display recent database and file archive runs, individual file/database sizes, and total archive sizes directly from the bucket. - Companion Plugin Sync: The daily
clockwork:push-companion-backupscommand feeds this Spaces history and SpinupWP schedule to the Companion plugin on each site, surfacing it to clients inwp-admin. - Offsite Glacier Relay: SpinupWP sites can also participate in the multi-provider Backup Relay pipeline (
SpinupWpBackupRelayAdapter), streaming long-term archives to AWS S3 Glacier Instant Retrieval.
Setup
-
Generate an API token in SpinupWP’s account settings.
-
Set in
.env:CLOCKWORK_SPINUPWP_TOKEN=... # Optional: set to true to block remote mutations and SSH execution CLOCKWORK_SPINUPWP_VIEW_ONLY=false -
Test:
php artisan clockwork:spinupwp-test
# Or using the standardized alias:
php artisan clockwork:test-spinupwp
Displays operating mode (Full Access or View Only) and lists servers and sites.
View-Only (Read-Only) Mode
When inspecting or auditing a third-party or client-owned SpinupWP fleet:
- Configuration: Set
CLOCKWORK_SPINUPWP_VIEW_ONLY=truein.envor toggle View-Only Mode in Settings → Integrations. Default isfalse. - Zero API Mutations:
POST,PUT, andDELETErequests are blocked and throwSpinupWpReadOnlyExceptionbefore hitting the API. - Zero Remote Writes: SSH command runner and Companion plugin deployment are disabled (
commandRunner()andcompanionInstaller()returnnull). - Gated Capabilities:
CAP_SSHandCAP_COMPANIONevaluate tofalsewhen view-only mode is active. - Dry-Run Import: Test fleet discovery without database commits using
php artisan clockwork:import-spinupwp --dry-run. - Diagnostic Reporting: Diagnostics at
/settings/diagnosticsappend· Mode: View Only.
Auth
Bearer token. Modules\SpinupWp\SpinupWpClient reads it via CredentialResolver (spinupwp.token — database first, config('clockwork.spinupwp.token')/.env fallback).
Endpoints we call
Base URL https://api.spinupwp.app/v1.
| Method | Path | Purpose |
|---|---|---|
| GET | /servers, /servers/{id} |
Inventory + per-server refresh. |
| GET | /sites, /sites/{id} |
Same for sites. The site response includes backups config (no history). |
| GET | /sites/{id}/events |
Recent events with graceful fallback for older accounts. |
| POST | /sites |
Migration runner only — create the destination site for a cutover. |
| POST | /sites/{id}/domains |
Migration runner only — add an additional domain. |
Files
modules/SpinupWp/src/SpinupWpClient.php— the HTTP client.modules/SpinupWp/src/SpinupWpHostingProvider.php— theHostingProvideradapter (Modules\Core\Contracts\HostingProvider): capabilities, credential fields, diagnostics check.modules/SpinupWp/src/SpinupWpServiceProvider.php— registers the client credential binding,HostingProvider, and diagnostics check with the module registry.app/Console/Commands/ImportSpinupWp.php— daily idempotent import.app/Console/Commands/SpinupWpTest.php— connectivity check.app/Services/Sites/WpConfigExtractor.php— first-time DB credential extraction (SSH).- Config:
config/clockwork.php→spinupwpkey.
Scheduled jobs that depend on it
| Cadence | Command |
|---|---|
| daily 03:30 | clockwork:import-spinupwp — re-run inventory; idempotent on servers.spinupwp_id and sites.domain. |
| daily 03:35 | clockwork:find-orphan-sites — runs right after to reclassify sites whose SpinupWP linkage was lost. |
| daily 04:15 | clockwork:poll-system-updates — uses the upgrade_required boolean SpinupWP imports as a gate, then SSHs to the small subset of servers actually flagged. The SpinupWP API only exposes the boolean — the count + per-package list comes from apt-check + apt list --upgradable over SSH and lands in server_update_snapshots. |
Idempotency
hosting_provideris set explicitly toSite::HOSTING_PROVIDER_SPINUPWPon every site the import creates. It used to fall back to a table-level default of'spinupwp'; that default has since been dropped (the column isNOT NULLwith no default now) so a future creation path that forgets to set it fails loudly instead of silently mislabeling a site as SpinupWP-hosted.servers.spinupwp_idis the natural key for re-import.- Manual-add reconciliation: a server added via
/servers/newhas nospinupwp_id. When the SpinupWP record for the same box later imports,upsertServerfalls back to matching by(hostname, ssh_port)against rows wherespinupwp_id IS NULL, adopts the SpinupWP id onto the manual row, and merges the SpinupWP-sourced fields. Without this, the import would collide on theservers_hostname_ssh_port_uniqueindex. - Deletion sweep: at the end of every import, any local Site or Server row whose
spinupwp_idis not in the API response has itsspinupwp_idnulled. This is what surfaces SpinupWP-side deletes toclockwork:find-orphan-sites(the import itself never deletes rows — it only severs the linkage so the orphan-finder can classify on the next tick). Counts surface in thespinupwp_id_nulledfield of theServers:andSites:summary lines. sites.domainis the unique natural key. Always bypass thenotArchivedglobal scope when looking up sites by domain in import paths:Site::withoutGlobalScopes()->firstOrNew(...). Without that bypass, a same-domain archived row collides atsave()-time on the unique index. If asite_ingest_exclusionsrow matches the domain (any provider) or this SpinupWP site id, the import skips (skipped_excluded) instead of updating that archived row — that’s what makes Remove from monitoring stick while the site is still live in SpinupWP.- All sites are imported, not just WordPress sites. Non-WP sites get
is_wordpress=falseand skip WP-specific data sources but still participate in nginx log tailing and IP banning. - New staging/dev domains get uptime monitoring auto-disabled on creation.
isStagingDomain()matchesstaging.*,dev.*,*.staging.*, and anything matching/-dev\./i— these should never fire a down alert. Only applies on first creation, never overrides a manual re-enable on an existing site. See Features → Uptime monitoring. - New sites start with
auto_updates_paused = false(creation-only, never overriding an existing manual setting). This ensures newly-imported sites participate in updates immediately unless intentionally paused. See Features → Updates.
On-demand refresh
The 03:30 daily import is the steady state, but two paths trigger an immediate re-run:
POST /servers/refresh-spinupwp— fleet-wide button, surfaces on the dashboard and every server header. Launchesclockwork:import-spinupwpthenclockwork:poll-serversin the background so the request cannot time out. A freshly-imported server flips out ofunknownonce the poll finishes.ServersController::store— launches the same background import after a manual server creation so any sites that already belonged to that box (per SpinupWP) attach without blocking the redirect.
