Files
website/docs/superpowers/specs/2026-04-26-vps-hosting-estimator-design.md
Andrew d5f97d1240 docs(spec): VPS hosting estimator + included list refresh design
Design captured from brainstorming: workload-driven estimator with
plan + add-on configurator (managed support + off-site backup tiers),
URL state, share link, checkout pre-fill, and rewritten Included list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:44:12 -04:00

20 KiB
Raw Permalink Blame History

VPS Hosting Page — Estimator + Included List Refresh

Date: 2026-04-26 Status: Approved (brainstorm), pending implementation plan Owner: Andrew Repo: EZSCALE/website

Goals

  1. Add an interactive estimator to the /vps-hosting page that lets visitors price a configuration (workload → plan → add-ons) without entering the order/checkout flow. The estimator does eventually link to checkout with the configuration pre-applied, but the estimator itself is read-only and bookmarkable.
  2. Rewrite the "Included With All Plans" card with accurate, expanded content and a "coming soon" badge for DDoS protection.
  3. Extend the add-on inventory with two new product lines: Managed Support tiers and Off-site Backup tiers.

Non-goals (future work)

  • Implementing the StorJ-backed backup orchestration (the add-on selection is captured at checkout, but backup execution is separate work).
  • Implementing DDoS protection.
  • Replicating the estimator on /dedicated-servers, /web-hosting, /game-servers. The same pattern can be applied later.
  • Save-and-email-quote flow.
  • Admin UI for editing the workload map (it stays hardcoded in the route closure).
  • Funnel analytics for estimator usage.

Page structure

After the change, /vps-hosting reads top-to-bottom:

1. Hero (unchanged)
2. EstimatorSection                          ← NEW
3. Features grid (unchanged)
4. Plans Table (unchanged + per-row "Pre-fill estimator" link)
5. Included With All Plans (rewritten content)
6. CTA (unchanged)

Estimator UX

Layout style

Progressive reveal on a single screen. No forced wizard steps.

┌─ Billing cycle:  [ Monthly | Quarterly | Annual ]            ┐
│                                                              │
│ What are you running?                                        │
│ [Personal site] [Web app] [Dev/staging] [Discord bot]        │
│ [VPN/proxy] [Storage target] [Database] [CI/CD] [Not sure]   │
│                                                              │
│ ─── (revealed once a workload is picked) ───                 │
│                                                              │
│ Recommended: VPS-4    2 vCPU · 4 GB RAM · 80 GB SSD   $15/mo │
│ [or pick another plan ▾]                                     │
│                                                              │
│ Customize add-ons ▾  (collapsed by default)                  │
│                                                              │
│ ─── sticky footer ───                                        │
│ Your total: $XX/mo (billed annually)                         │
│ [Order this configuration]   [Copy share link]               │
└──────────────────────────────────────────────────────────────┘

Workload picker

9 chips. Clicking sets the workload and the recommended plan. "Not sure" opens the mini-quiz dialog instead.

Workload → plan mapping

Workload key Default plan Alternates shown
personal_site VPS-2 ($8) VPS-1, VPS-4
saas VPS-4 ($15) VPS-2, VPS-8
dev_staging VPS-2 ($8) VPS-1, VPS-4
discord_bot VPS-1 ($5) VPS-2
vpn VPS-1 ($5) VPS-2
storage_target Storage 1TB ($28) Storage 500, VPS-4
database VPS-16 ($55) VPS-8, VPS-32
cicd VPS-8 ($30) VPS-4, VPS-16
not_sure (mini-quiz)

Mini-quiz (for not_sure)

Step 1 — closest match (always shown):

12 example apps in a card grid:

App Maps to
WordPress / static blog VPS-1
Ghost / Mastodon (small) VPS-2
Bitwarden / Vaultwarden VPS-1
Plex / Jellyfin Storage 500
Nextcloud / file sync Storage 1TB
Gitea / self-hosted git VPS-2
Mattermost / chat VPS-4
Postgres / MySQL DB VPS-16
Mautic / marketing app VPS-4
ELK stack / logging VPS-8
Docker swarm / k3s VPS-8
Other / not listed (Step 2)

Step 2 — only if "Other" picked:

  1. Expected traffic? Low (~100/day) / Medium (~10k/day) / High (~100k/day)
  2. Most important? Cheapest / Reliability / Speed / Storage-heavy

The quiz returns a recommended plan with one-line reasoning ("VPS-4 fits because medium traffic + reliability priority").

Add-ons panel

Collapsed by default; expands on click. Inside:

  • IPv4 stepper — 1 to 8. First IPv4 is included; extras are $8/each.
  • Windows License — toggle, free (BYOL).
  • Managed Support — 4-option radio. Pilot disabled when plan is below VPS-8 (tooltip + "Upgrade plan" link).
  • Off-site Backup — 5-option radio with retention/quota helper text.

Pilot tier gating

ManagedSupportSelector reads the current plan. If it is below VPS-8 (i.e., VPS-1, VPS-2, VPS-4, Storage 500, Storage 1TB), the Pilot radio is disabled with tooltip "Pilot requires VPS-8 or higher. [Upgrade plan]". Clicking the upgrade link sets the plan to VPS-8 and re-enables Pilot.

If the user already has Pilot selected and downgrades, the selection auto-falls back to Pro and a snackbar warns "Pilot tier removed — requires VPS-8 or larger."

Billing cycle handling

Three options: Monthly, Quarterly (5% discount), Annual (15% discount). Semi-annual is omitted from the toggle to keep it tidy; existing pricing-page math stays as-is.

The base plan and every add-on already have per-cycle prices in the database (plan_prices and plan_config_values rows). The estimator looks up the matching cycle row for each item and sums them — it does not multiply by a percentage. This guarantees parity with the pricing page and the actual checkout total.

URL state and sharing

On mount, the estimator parses the query string and hydrates state. Recognized params:

  • w — workload key (saas, database, ...)
  • plan — plan id (numeric)
  • ipv4 — number 1-8
  • windows0 | 1
  • managedself | basic | pro | pilot
  • backupnone | lite | standard | extended | vault
  • cyclemonthly | quarterly | annual

Missing params get sensible defaults (workload null, planId null, all add-ons off, cycle monthly). Invalid params are silently ignored (e.g., plan=999 → ignored; managed=pilot&plan=vps-1 → falls back to managed=pro plus a snackbar warning).

After hydration, any state change debounces (300 ms) and updates the URL via history.replaceState — no scroll, no reload, browser back/forward works.

  • Order this configuration — primary, navigates to {accountUrl}/checkout/{plan_id}?ipv4=2&windows=1&managed=pilot&backup=standard&cycle=annual. The existing checkout route reads these query params and pre-selects the matching config options.
  • Copy share link — secondary, copies window.location.href to clipboard. Snackbar: "Link copied — anyone with this link sees the same configuration."

Total calculation

cycleTotal =
  plan.prices.find(p => p.billing_cycle === cycle).price
  + (ipv4Count - 1) * ipv4ConfigValue.prices[cycle]
  + windowsConfigValue.prices[cycle]                          // always $0
  + managedTierConfigValue.prices[cycle]                      // chosen tier
  + backupTierConfigValue.prices[cycle]                       // chosen tier

When cycle === 'monthly', the value is per month. When cycle === 'quarterly', it is the 3-month bill. When cycle === 'annual', the 12-month bill. The footer also shows an "effective monthly" line: cycleTotal / months.

Backend

Database

No schema changes. The existing plan_config_groups / plan_config_options / plan_config_values schema covers everything.

database/seeders/ConfigOptionSeeder.php is extended to seed two new groups:

Managed Support group (NEW)

Value name hourly monthly quarterly semi_annual annual metadata
Self-Managed 0 0 0 0 0 is_default: true
Managed Basic 0.04 29 82.65 156.60 295.80
Managed Pro 0.11 79 225.15 426.60 805.80
Pilot 0.14 99 282.15 534.60 1009.80 min_plan_tier: 8

(Quarterly/semi-annual/annual prices computed from monthly at the standard 5% / 10% / 15% discounts that the existing seeder uses for other groups; reuse quarterlyPrice() / semiAnnualPrice() / annualPrice() helpers in the seeder.)

The metadata.min_plan_tier: 8 field on the Pilot value is the gating signal. The frontend reads it via the eager-loaded addOns Inertia prop. Plans expose a numeric "tier" — VPS-1 = 1, VPS-2 = 2, VPS-4 = 4, VPS-8 = 8, VPS-16 = 16, VPS-32 = 32, Storage 500 = 4 (treated as VPS-4 equivalent), Storage 1TB = 4. Plan tier is derived from features.cpu × features.ram heuristic, or simpler: a metadata.tier field added to each plan row in the existing PlanSeeder.php.

Off-site Backup group (NEW)

Value name hourly monthly quarterly semi_annual annual metadata
None 0 0 0 0 0 is_default: true
Lite 0.007 5 14.25 27.00 51.00 retention_days: 7, quota_gb: 200
Standard 0.017 12 34.20 64.80 122.40 retention_days: 30, quota_gb: 500
Extended 0.035 25 71.25 135.00 255.00 retention_days: 90, quota_gb: 1024
Vault 0.082 59 168.15 318.60 601.80 retention_days: 365, quota_gb: 2048

The metadata fields (retention_days, quota_gb) are surfaced to the frontend so the Backup selector can show "Daily backups, 30-day retention, up to 500 GB" helper text per option without hardcoding.

Route

routes/marketing.php — the /vps-hosting closure adds two more props:

  • addOnsPlanConfigGroup::where('service_type', 'vps')->with('options.values')->get() (eager loaded).
  • workloadMap — inline static array mapping workload keys → plan slugs (default + alternates) and the 12 mini-quiz apps. Kept in the route closure for now; if it grows or needs reuse, extract to config/marketing.php.

The Inertia::render('Marketing/VpsHosting', ...) call passes both alongside the existing plans prop.

Checkout pre-fill

The existing account.<domain>/checkout/{plan} route already supports config option selection. We need to verify (during implementation) that it accepts the query-string keys ipv4, windows, managed, backup, cycle and pre-populates the corresponding form fields. If it currently only reads from the subscription_config_selections table (post-checkout), a small enhancement to the checkout controller is required — read query params and seed the form's initial state.

Frontend

File map

resources/ts/
├─ Pages/Marketing/VpsHosting.vue              (modified)
├─ Components/Marketing/Estimator/             (NEW)
│   ├─ EstimatorSection.vue
│   ├─ BillingCycleToggle.vue
│   ├─ WorkloadPicker.vue
│   ├─ MiniQuizDialog.vue
│   ├─ RecommendedPlanCard.vue
│   ├─ AddOnsPanel.vue
│   ├─ IPv4Stepper.vue
│   ├─ ManagedSupportSelector.vue
│   ├─ BackupTierSelector.vue
│   └─ EstimatorFooter.vue
└─ stores/estimator.ts                          (NEW Pinia store)

Pinia store

Single source of truth.

state = {
  workload: string | null,
  planId: number | null,
  ipv4Count: number,                  // 1..8
  windowsLicense: boolean,
  managedTier: 'self' | 'basic' | 'pro' | 'pilot',
  backupTier: 'none' | 'lite' | 'standard' | 'extended' | 'vault',
  cycle: 'monthly' | 'quarterly' | 'annual',
}

getters = {
  recommendedPlanId,                  // workload + workloadMap
  monthlyEffectiveTotal,              // base + add-ons normalized to /mo
  cycleTotal,                         // sum of cycle-specific prices
  shareUrl,                           // /vps-hosting + query string
  checkoutUrl,                        // accountUrl + checkout link
  pilotAvailable,                     // boolean: planId tier >= 8
}

actions = {
  hydrateFromUrl,
  setWorkload,
  setPlan,                            // also re-evaluates managedTier (auto-fallback)
  setManagedTier,
  // ...
}

Component contracts

Component Owns Talks to store
EstimatorSection Layout, scroll-reveal animations, mini-quiz mounting reads/writes everything
BillingCycleToggle UI for cycle (chip group) writes cycle
WorkloadPicker 9 chips with icons writes workload, planId
MiniQuizDialog 12-app grid + 2 follow-up Qs writes workload, planId
RecommendedPlanCard Display + "change plan ▾" dropdown reads/writes planId
AddOnsPanel Collapse/expand, holds 4 sub-controls reads all
IPv4Stepper +/- buttons, validation writes ipv4Count
ManagedSupportSelector 4 radios + Pilot disabled state reads planId, writes managedTier
BackupTierSelector 5 radios with retention helper writes backupTier
EstimatorFooter Sticky positioning, total display, 2 CTAs reads cycleTotal, checkoutUrl, shareUrl

Edge cases

Case Behavior
Shared URL with plan=999 (invalid) Ignored; falls back to recommendation if workload set, else planId stays null
Shared URL with managed=pilot&plan=vps-1 Auto-fallback to managed=pro; snackbar warning
addOns Inertia prop empty (seeder not run) AddOnsPanel hidden; "Order this configuration" still functions with base plan
User picks workload, then changes plan to one that invalidates Pilot Toast: "Pilot tier removed — requires VPS-8 or larger"
Mobile (< 600 px) Single column; footer docks to viewport bottom; AddOnsPanel collapsed by default
Workload picker on mobile 2-column chip grid (vs 3-col desktop); MiniQuiz goes full-screen
Browser back/forward URL state means estimator restores; no SPA-traps
cycle=annual in shared URL Annual toggle pre-selected on landing
Plans Table "Pre-fill estimator" link Sets planId only; leaves workload null

"Included With All Plans" — final list

Hardcoded in VpsHosting.vue (no backend config). 13 items + 1 "coming soon" badge.

1.  1 free IPv4 + 1 /64 IPv6 block
2.  IPv4 rDNS / PTR control
3.  10 Gbps shared uplink (fair-use per AUP)
4.  RAID 10 SSD storage
5.  ZFS storage snapshots (free)
6.  KVM virtualization
7.  Full root access
8.  Linux & Windows (BYOL) support
9.  VirtFusion control panel
10. Out-of-band console / VNC access
11. Near-instant provisioning
12. 99.9% uptime SLA
13. 14-day money-back guarantee

Coming soon (badge): DDoS protection

Testing

Pest feature tests — tests/Feature/Marketing/VpsHostingEstimatorTest.php

  • vps-hosting page loads with addOns prop populated
  • vps-hosting page exposes workloadMap with all 9 keys
  • seeder creates managed support group with 4 values including Pilot
  • seeder creates backup tier group with 5 values
  • pilot value has metadata.min_plan_tier = 8
  • checkout route accepts ipv4, windows, managed, backup, cycle query params and pre-selects them

Vitest unit tests — resources/ts/stores/estimator.test.ts

  • monthlyEffectiveTotal correctly sums plan + add-ons across cycles
  • cycleTotal returns the cycle-specific row price (not a calculated discount)
  • recommendedPlanId returns the expected plan for each of 9 workloads
  • pilot is disabled when plan tier < 8
  • URL hydration parses all 7 query params correctly
  • URL hydration falls back gracefully on invalid planId
  • URL hydration auto-fallback fires when managed=pilot but plan=vps-1
  • shareUrl reflects current state

Playwright E2E (one happy path)

tests/e2e/vps-estimator.spec.ts:

  1. Open /vps-hosting
  2. Click "Web app / SaaS backend" → assert VPS-4 appears
  3. Expand add-ons; assert Pilot radio is disabled
  4. Click "Upgrade plan" → assert plan switches to VPS-8 and Pilot is selectable
  5. Pick Pilot + Backup Standard + Annual cycle
  6. Assert footer total equals VPS-8 annual + Pilot annual + Backup Standard annual (computed from seeded prices)
  7. Click "Copy share link" → URL contains plan=, managed=pilot, backup=standard, cycle=annual

Open implementation questions

These are deferred to the implementation plan, not blockers for the spec:

  • Verify the existing account.<domain>/checkout/{plan} controller reads query params for config pre-selection. If not, add the param-handling code path.
  • Confirm whether plan tier is best derived from metadata.tier (added to PlanSeeder) or from a heuristic on features.cpu × features.ram. Recommendation: explicit metadata.tier field — clearer, less brittle.
  • Decide whether the workload map and 12-app list live inline in the route closure or in config/marketing.php. Default to inline; promote later if reused.

Acceptance criteria

  • EstimatorSection appears between hero and Features grid on /vps-hosting.
  • All 9 workload chips work; "Not sure" opens the mini-quiz.
  • Mini-quiz Step 1 (12 apps) maps each app to the documented plan.
  • Mini-quiz Step 2 fires only when "Other" is picked.
  • Add-ons panel shows IPv4 / Windows / Managed / Backup with correct pricing per cycle.
  • Pilot is disabled below VPS-8 with tooltip + Upgrade Plan link.
  • Auto-fallback fires when downgrading from a Pilot-eligible plan.
  • Billing cycle toggle (Monthly / Quarterly / Annual) updates totals using the seeded per-cycle price rows.
  • URL state hydrates and updates correctly; shared link fully restores configuration.
  • "Order this configuration" navigates to checkout with all params; checkout pre-selects them.
  • "Copy share link" copies window.location.href and shows a snackbar.
  • "Included With All Plans" card displays 13 items + DDoS coming-soon badge.
  • Plans Table rows have "Pre-fill estimator" links that set planId.
  • All Pest, Vitest, and Playwright tests pass.
  • Mobile (< 600 px) layout works without horizontal scroll; footer is bottom-docked.
  • Existing npm run build and php artisan test --compact both pass.