Product design v2 · 5 of 6

Maintenance & modifications

Every change to a pool, whether the client, a staff member or the system asks for it, goes through one path: a change row, a guarded job, an AWS or licence-provider call, and an applied row. Billable changes move the billing timeline; sites and licence tiers are logged but never billed.

StatesSites on the poolLicence tiersEndpointsJobs, guards, rollbackChange-log rulesSuspend, stop, rebootLimits and monitoringWHMCS adminIn the app

States

stateDiagram-v2
  [*] --> queued: order (card + standing checks)
  queued --> provisioning: job starts
  provisioning --> active: step 20 activate
  provisioning --> failed: max exceptions
  failed --> provisioning: resume command
  failed --> terminating: abort command
  active --> suspended: WHMCS suspend / staff
  suspended --> active: unsuspend
  active --> cancellation_pending: cancel request
  cancellation_pending --> active: cancel_termination (inside window)
  cancellation_pending --> terminating: due date / abort window passed
  terminating --> closing_account: instance gone, final invoice
  closing_account --> closed: CloseAccount succeeded
  closed --> [*]

Instance state (running, stopped, rebooting) is separate from these lifecycle states and is read live from EC2; the cached copy in dedicated_instances.instance_state only serves list pages.

Sites on the pool

From the app

"Create Site" offers Rochen One (shared) or any of the client's pools. With a pool chosen, the order carries the new product Rochen One Dedicated Site; RochenOne::store() routes it to Dedicated::storeSite(): parent_id = pool, server = the pool's own tblservers row, the effective limits and disk_size quota in custom fields (no configurable options; the tier picker only fills the fields), no container option updates and no rochen_one_logs row. The existing deploy then creates the cPanel account, DNS, quota, LVE limits, Softaculous and mail on the pool's server.

Resource limits, free

A Dedicated Site keeps the Server Resources picker (Tier 1–8 presets) and gains a custom form for speed, pmem, io, iops, ep and nproc; either way the values are applied with the existing applyLVELimits on the pool and written to the site's custom fields. The pool page shows quotas and limits summed against the pool's allocation and instance size and warns when oversubscribed; a single quota larger than the pool's volume is refused. Changes are logged on the site's history and never billed. The client has no WHM or root access.

Delete

Deleting an app site on a pool removes the cPanel account on the pool's server and terminates the row; no container maths, no log row. A site_removed change row triggers a re-tier check.

Manage like Shared

Every site page a shared site has (details, resources, disk, PHP version, monitoring, delete) works for a pool site unchanged; only the price labels disappear.

Licence tiers

RetierDedicatedLicencesJob counts the live Dedicated Site services on the pool (parent_id = pool), applies the rules in config/dedicated.php (cPanel by accounts; Imunify360 by accounts; LiteSpeed by instance size and accounts; CloudLinux, KernelCare, JetBackup, MailChannels fixed), and changes the provider package or key limit when the tier differs from dedicated_instances.licences.

TriggerDirectionWhat happens
Site created, or the daily dedicated:retier-licences reconcile finds a mismatchupat once: Manage2 change package / CLN update key limit; tracking options and licences json updated; change_licence row (not billable)
Site deleteddownonly after 24 hours below the threshold, to avoid flapping
Template applied (resize)eitherLiteSpeed re-evaluated for the new instance size
Provider call fails—row failed, retried next day; Maintenance ticket after 3 failures. LiteSpeed changes open a ticket until an API is confirmed
Daily reconcile finds cPanel accounts on the server that are not app rows—Maintenance ticket; tiers follow the app rows

Endpoints

CallPayloadHandled byNotes
PATCH /v1/hosting/{id}{template} or {diskSize: [{device, size, iops, throughput, mountPoint}]}Dedicated::updateHosting()same payloads the AWS server pages already send, so servers/[id]/resources and /disk work unchanged
PATCH /v1/hosting-configuration/{id}{command: reboot_server | start_server | stop_server}DedicatedConfigurationControllersynchronous EC2 calls; each writes a change row
PATCH /v1/hosting-configuration/{id}{command: cancel_termination}sameallowed only while cancellation_pending
GET /v1/hosting-configuration/{id}?command=state—existinglive EC2 state, polled by the details card
GET …?command=provisioning—newplan progress while queued / provisioning / terminating
GET …?command=history—newthe change log, newest first, paginated
GET …?command=licences—newcurrent tier per software, the account count that drives it, and when it was last checked
GET …?command=sites—newthe sites on the pool with their limits

HostingPolicy refuses update and upgrade while DedicatedInstance::inFlightFor() or DedicatedServerChange::inFlightFor() is true, the same rule the migration flow uses. Every endpoint is added to public/docs/api.html.

Jobs, guards, rollback

All jobs are unique per hosting on the dedicated queue with the lock dedicated:change:{hosting}. Each starts by moving its change row to applying and ends with applied, failed or rolled_back.

JobGuardsStepsRollback
ResizeDedicatedInstanceJobcan_scale · template belongs to the pool's region group · differs from current · no change in flight · instance running or stoppedstop → wait stopped → ModifyInstanceAttribute type → start → wait running → change applied (new billing span), Templates option updated → LiteSpeed re-tier queuedmodify fails → start with the old type, change rolled_back; start fails → change failed, Maintenance ticket, client mail
ResizeDedicatedDiskJobgrow only · 6 h since the last applied resize_disk · gp3 ratio: throughput ≤ IOPS ÷ 4, IOPS ≤ 500 × GB · within the region's min/maxModifyVolume → wait past modifying → SSM command: pvresize, lvextend, xfs_growfs /home → change applied, options updatedModifyVolume failed → nothing changed, failed. Filesystem grow failed → the volume did grow, so the change is applied with an error and a ticket "filesystem not grown"
reboot · start · stopcan_reboot · instance state allows itone EC2 call, synchronous, change row writtennone needed
RetierDedicatedLicencesJoblive count available · tier differs · 24 h hysteresis on the way downprovider change package or key limit → licences json + tracking options → change_licence row (not billable)provider call failed → row failed, retried; nothing else changed
RotateDedicatedKeyJob (staff only, dedicated:rotate-key)instance runningnew key pair → SSM command replaces authorized_keys → old key pair deleted → refs updatedSSM command failed → new key deleted, old kept

Change-log rules

One row per intent

Created with status = requested and the actor (client, admin, system) before any job is dispatched, so a request that never ran is still visible.

Whole configuration

to holds the full resulting configuration, never a delta, so any row can be read on its own and the billing routine needs no replay.

Immutable once applied

applied_at is written once. Mistakes are corrected with a new reconcile row, which is how a manual AWS-side change gets recorded too.

Billable flag

Only create, resize_instance, resize_disk and terminate move the billing timeline. Reboots, key rotations and licence changes are logged but do not start a span.

Audit

Every applied or failed change also writes a tbllogs row; dedicated:show {hosting} prints configuration, plan and log for support.

Drift check

dedicated:reconcile (weekly) compares instance type, volume size, IOPS and throughput against EC2 and opens a Maintenance ticket on mismatch, replacing the legacy matchInstances mails.

Suspend, stop, reboot

ActionWhoInstanceService statusLicencesBilling
SuspendWHMCS overdue automation or staffstoppedSuspendeduntouchedcontinues (type and volumes reserved)
UnsuspendWHMCS on payment, or staffstartedActiveuntouchedcontinues
Stopclient (can_reboot)stoppedActiveuntouchedcontinues; the app says so next to the button
Start · Rebootclientstarted / rebootedActiveuntouchedcontinues

Limits and monitoring (proposals)

Instance-size ceiling

Accounts with fewer than 3 paid months, or any overdue history, may run one pool up to m7i.xlarge; established accounts up to m7i.4xlarge; larger sizes or more pools need a staff approval recorded on the client. The store and the resize page hide sizes above the ceiling and say why. Thresholds to agree with Chris and Ben.

New pool alert

Every deployment mails devops with client, account age, fraud score, template, region and hostname, so new accounts get a human look early, as Chris asked.

Transfer and abuse alerts

dedicated:log-data-transfer opens a Maintenance ticket when a pool younger than 30 days passes 1 TB outbound in 7 days, or any pool passes 5 TB in a month; Zabbix keeps the usual host alerts. Staff can suspend from WHMCS, which stops the instance.

Credentials

Each pool's secrets live in SSM for the instance and as one item in the 1Password inbox vault for staff; the deployer can write there and nowhere else.

WHMCS admin

WHMCS still calls the aws module for product 464 (suspend, unsuspend, terminate, the Start/Stop buttons, and a template change on every service save). A minimal guard keeps those calls from touching the legacy pipeline:

Guard
aws_isDedicated($serviceId) — true when a dedicated_instances row exists
Affected functions
aws_CreateAccount, aws_SuspendAccount, aws_UnsuspendAccount, aws_StartServer, aws_StopServer, aws_AdminServicesTerminate, aws_AdminServicesTabFieldsSave
Behaviour
for a Dedicated service they insert a dedicated_server_changes row (actor = admin, status = requested) and return success; they never write to the aws queue
Applied by
dedicated:apply-requested-changes every minute, dispatching the matching job
Templates
the new admin page configdedicatedtemplates.php edits dedicated_templates; service configuration itself is edited in the app or through the API, not in the WHMCS service tab

In the app

Server page

Reuses the AWS server cards: details with live state and Start/Stop, resources with the template picker, one card per volume, Zabbix graphs. Adds a provisioning card (step list while queued, provisioning or terminating), the Sites and Software cards, and a History card fed by ?command=history, styled like Rochen One's history.

Resize pages

servers/[id]/resources and /disk work as they do for MCS; their guard accepts product.line === "dedicated". The review block states that a resize restarts the server and when the new rate starts.

Sites and software cards

New on the pool page: the sites on the pool with a "Create Site" shortcut, and the included software with the current tier of each and the site count that set it. Nothing to buy.

No access page

The client has no root, SSH or WHM. The pool's hosting row has null credentials, like the Shared container; the root password and WHM token live once, on the pool's tblservers row, WHMCS-encrypted. Support reaches the server over SSM or with that credential; the API never exposes it.

Rochen One Dedicated · maintenance v2 · September 2026