Files
website/docs/superpowers/specs/2026-03-14-vps-pricing-overhaul-design.md
Claude Dev b4ef90465c feat: complete pre-launch audit — frontend polish, churn prevention, login history, financial reports, configurable checkout
Includes all work from phases 6-9+ and frontend polish rounds 1 & 2:

- Login history with device trust, new device notifications, session management
- Churn prevention: cancellation surveys, winback campaigns with email sequences
- Financial reports: revenue, P&L, tax, aging, refund, subscription reports with PDF/CSV/JSON export
- Configurable checkout: plan config groups/options, build-your-own VPS
- Frontend polish: fix broken legal links, add SEO meta tags, favicon, font display=swap,
  Head titles on all 14 marketing pages, mobile responsive fixes, AuthLayout legal footer,
  remove false 24/7 claims, hide empty stats, correct uptime SLA to 99.9%,
  GameServers notify buttons linked to /contact, 301 redirects for /terms and /privacy
- WHMCS migration scripts
- Update legal page effective dates to March 16, 2026

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:39:25 -04:00

15 KiB
Raw Blame History

VPS Pricing Overhaul — Design Spec

Overview

Replace the current flat pricing model (one price per plan, monthly only) with a multi-cycle pricing system (1/3/6/12 months), updated VPS plan tiers reflecting actual infrastructure costs ($1,500/mo datacenter), hardware generation (Xeon E5-2680 v2/v4), unmetered bandwidth, and tiered disk I/O limits. Includes migration path for all existing customers and VirtFusion package deprecation.

Context

Infrastructure

  • 3 hypervisor nodes in Atlanta (E5-2680 v2/v4, 1,158 GB RAM, NFS-backed SATA SSD storage)
  • Monthly infrastructure cost: $1,500
  • 82 VMs total, ~14 are EZSCALE internal, ~68 customer
  • RAM is 89% allocated — the binding constraint
  • Storage is NFS-backed SATA SSD (not local NVMe)

Problems with current pricing

  • Plans named inconsistently (Nano/Micro/Mini/Standard/Plus/Pro vs VirtFusion's Micro/Mini/Basic/Standard/Advanced/Pro)
  • Only monthly billing — no commitment incentives, higher churn
  • Prices too low for actual cost basis ($3.50 Nano loses money at $22/VM cost)
  • Stripe only gets one price per plan — multi-cycle discounts calculated client-side only
  • IPv4 addon ($3/mo) not charged through Stripe
  • Bandwidth caps listed but offering unmetered (AUP)

New Plan Lineup

VPS Plans

Plan Slug vCPU RAM SSD Bandwidth IOPS R/W MB/s R/W
vps-1 1 1 GB 25 GB Unmetered 2,500/2,500 40/40
vps-2 1 2 GB 50 GB Unmetered 3,000/3,000 50/50
vps-4 2 4 GB 80 GB Unmetered 4,000/4,000 75/75
vps-8 4 8 GB 160 GB Unmetered 5,000/5,000 100/100
vps-16 6 16 GB 320 GB Unmetered 6,000/6,000 150/150
vps-32 8 32 GB 640 GB Unmetered 8,000/8,000 200/200

Storage VPS Plans

Plan Slug vCPU RAM SSD Bandwidth IOPS R/W MB/s R/W
stor-500 2 2 GB 500 GB Unmetered 3,000/3,000 75/75
stor-1tb 2 4 GB 1 TB Unmetered 4,000/4,000 100/100

Pricing (USD)

Plan 1-mo 3-mo (5% off) 6-mo (10% off) 12-mo (15% off)
VPS-1 $5.00 $14.25 $27.00 $51.00
VPS-2 $8.00 $22.80 $43.20 $81.60
VPS-4 $15.00 $42.75 $81.00 $153.00
VPS-8 $30.00 $85.50 $162.00 $306.00
VPS-16 $55.00 $156.75 $297.00 $561.00
VPS-32 $99.00 $282.15 $534.60 $1,009.80
STOR-500 $18.00 $51.30 $97.20 $183.60
STOR-1TB $28.00 $79.80 $151.20 $285.60

Addons

  • Extra IPv4: $3.00/mo per address (billed per cycle with same discount tier)
  • /64 IPv6: included with every VPS

I/O Limits

  • Applied as internal VirtFusion package settings only
  • NOT advertised on pricing page
  • Scale with tier to prevent single-customer abuse of shared NFS storage

Customer Migration Map

Package → Plan Mapping

Old Package (VirtFusion ID) Old Specs New Plan New Specs Spec Changes
Micro (19, 32) 1 vCPU, 1 GB, 25 GB, 500 GB BW VPS-1 1 vCPU, 1 GB, 25 GB, Unmetered BW upgrade
Mini (20) 1 vCPU, 2 GB, 50 GB, 4 TB BW VPS-2 1 vCPU, 2 GB, 50 GB, Unmetered BW upgrade
Basic (21) 2 vCPU, 4 GB, 80 GB, 6 TB BW VPS-4 2 vCPU, 4 GB, 80 GB, Unmetered BW upgrade
Standard (22) 4 vCPU, 8 GB, 160 GB, 8 TB BW VPS-8 4 vCPU, 8 GB, 160 GB, Unmetered BW upgrade
Advanced (23) 6 vCPU, 16 GB, 320 GB, 10 TB BW VPS-16 6 vCPU, 16 GB, 320 GB, Unmetered BW upgrade
Pro (24) 8 vCPU, 32 GB, 640 GB, 16 TB BW VPS-32 8 vCPU, 32 GB, 640 GB, Unmetered BW upgrade
Dev Starter (40) 2 vCPU, 2 GB, 60 GB, 4 TB BW VPS-4 2 vCPU, 4 GB, 80 GB, Unmetered RAM +2 GB, disk +20 GB
Storage Box (41) 2 vCPU, 2 GB, 500 GB, 8 TB BW STOR-500 2 vCPU, 2 GB, 500 GB, Unmetered BW upgrade
RAM Optimized (42) 4 vCPU, 16 GB, 240 GB, 10 TB BW VPS-16 6 vCPU, 16 GB, 320 GB, Unmetered CPU +2, disk +80 GB
VPS-3-Custom (11) 4 vCPU, 8 GB, 60 GB, 4 TB BW VPS-8 4 vCPU, 8 GB, 160 GB, Unmetered Disk +100 GB
Base Package (43) 2 vCPU, 1 GB, 10 GB, 200 GB BW VPS-1 1 vCPU, 1 GB, 25 GB, Unmetered CPU -1, disk +15 GB

Deprecation Actions

  • All old packages set to status 'archived' in the plans table
  • Old VirtFusion packages disabled (not deleted, for audit trail)
  • New VirtFusion packages created with new I/O limits
  • Existing VMs migrated to new packages in VirtFusion (package swap, no rebuild)

Special Case: Base Package (ID 43)

Loses 1 vCPU (2→1). Check if any customer on this package needs 2 cores. If so, migrate to VPS-4 instead of VPS-1.

Database Changes

New Table: plan_prices

CREATE TABLE plan_prices (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    plan_id BIGINT UNSIGNED NOT NULL,
    billing_cycle ENUM('monthly', 'quarterly', 'semi_annual', 'annual') NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stripe_price_id VARCHAR(255) NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE,
    UNIQUE(plan_id, billing_cycle)
);

Plans Table Changes

  • Existing price column retained as base monthly price (for display/sorting)
  • Existing billing_cycle column retained for backward compatibility
  • New plans use plan_prices for actual billing; plans.price = monthly base price

No Schema Changes Needed

  • subscriptions.billing_cycle already supports monthly/quarterly/semi_annual/annual
  • subscriptions.provisioning_config already supports additional_ipv4 count
  • plans.features JSON already stores arbitrary plan metadata

Application Changes

New: PlanPrice Model

  • belongsTo(Plan)
  • $fillable = ['plan_id', 'billing_cycle', 'price', 'stripe_price_id']
  • $casts = ['price' => 'decimal:2']

Updated: Plan Model

  • Add prices(): HasMany(PlanPrice) relationship
  • Add priceForCycle(string $cycle): ?PlanPrice helper method (queries plan_prices for the given cycle)
  • Deprecate direct use of plans.stripe_price_id — keep column for backward compat but all new code uses PlanPrice->stripe_price_id

Updated: PlanSeeder

  • Create 8 new plans with features JSON:
    {
      "vcpu": "2",
      "ram": "4 GB",
      "storage": "80 GB SSD",
      "bandwidth": "Unmetered",
      "ipv4": "1 Included",
      "ipv6": "/64 Included",
      "iops_read": 4000,
      "iops_write": 4000,
      "mbps_read": 75,
      "mbps_write": 75,
      "virtfusion_package_id": null
    }
    
  • Create 4 plan_prices per plan
  • Archive old plans (status → 'archived') but do NOT delete — existing subscriptions still reference them
  • Include migration_map array mapping old plan slugs to new ones

Updated: SyncStripePrices Command

  • Fix semi_annuallysemi_annual: The existing command uses semi_annually in two match arms (lines 46, 54). Normalize to semi_annual to match the rest of the codebase (CheckoutController, StorePlanRequest, frontend).
  • Iterate plan_prices rows instead of plans
  • Create one Stripe Product per Plan
  • Create one Stripe Price per PlanPrice row:
    • monthly → interval: 'month', interval_count: 1
    • quarterly → interval: 'month', interval_count: 3
    • semi_annual → interval: 'month', interval_count: 6
    • annual → interval: 'year', interval_count: 1
  • Store stripe_price_id on each PlanPrice row
  • Create a separate Stripe Product for "Additional IPv4 Address"
  • Create 4 Stripe Prices for IPv4 addon (one per cycle, with discount baked in):
    • monthly: $3.00/mo
    • quarterly: $8.55/3mo ($3 × 3 × 0.95)
    • semi_annual: $16.20/6mo ($3 × 6 × 0.90)
    • annual: $30.60/yr ($3 × 12 × 0.85)
  • Store IPv4 addon Stripe price IDs in config or a dedicated addon_prices config array

Updated: CheckoutController

  • Validate billing_cycle from request
  • Look up PlanPrice for selected plan + cycle
  • Pass billing cycle to billing service (service resolves price internally)
  • Handle IPv4 addon quantity from configuration.additional_ipv4

Updated: BillingServiceInterface

  • No signature change. The existing createSubscription(User, Plan, ?paymentMethodId, ?couponCode, billingCycle) signature already accepts billingCycle.
  • Services resolve the correct PlanPrice internally via $plan->priceForCycle($billingCycle)->stripe_price_id
  • This keeps the interface stable and avoids breaking PayPal.

Updated: StripeBillingService

  • createSubscription(): Use $plan->priceForCycle($billingCycle)->stripe_price_id instead of $plan->stripe_price_id
  • Add IPv4 addon as additional subscription item using Cashier's multi-price API:
    $subscription = $user->newSubscription($plan->slug)
        ->price($planPrice->stripe_price_id)
        ->price($ipv4PriceId, $additionalIpv4Count) // only if count > 0
        ->create($paymentMethodId);
    
  • swapSubscription(): Must also resolve cycle-specific price. Accept billing cycle parameter, use $newPlan->priceForCycle($billingCycle)->stripe_price_id for the swap. If customer is swapping plans but keeping the same cycle, pull cycle from the existing subscription record.
  • Calculate current_period_end based on billing cycle

Updated: Pricing.vue (Marketing)

  • Add billing cycle toggle: Monthly | Quarterly | Semi-Annual | Annual
  • Show savings percentage on non-monthly cycles
  • Display cycle-specific price from plan_prices data (plans eager-loaded with prices)
  • Pass selected cycle to checkout URL as query param
  • TypeScript interface:
    interface PlanPrice {
      id: number
      plan_id: number
      billing_cycle: 'monthly' | 'quarterly' | 'semi_annual' | 'annual'
      price: string
    }
    
  • Plan interface updated: prices: PlanPrice[]

Updated: Checkout/Show.vue

  • Pre-select billing cycle from URL query param (or default monthly)
  • Show cycle-specific price from plan_prices
  • IPv4 addon total uses cycle-specific IPv4 Stripe price (discount already baked in)
  • Display breakdown: plan price + IPv4 addon = total

Updated: Admin Plans Edit

  • Add price fields for all 4 billing cycles
  • Save to plan_prices table
  • Trigger Stripe price sync on save

Updated: MarketingController

  • Eager-load plan_prices when passing plans to Pricing.vue:
    $plans = Plan::where('status', 'active')
        ->where('service_type', 'vps')
        ->with('prices')
        ->orderBy('sort_order')
        ->get();
    

Existing Subscription Handling

Archived plans remain functional

  • Archived plans are hidden from new purchases (Pricing.vue, Checkout) via status = 'active' filter
  • Archived plans are NOT filtered from subscription lookups, dashboard display, or webhook handlers
  • Customer dashboard shows the plan name from whatever plan_id is on their subscription — works for both active and archived plans
  • Webhook handlers that look up subscriptions by stripe_subscription_id are unaffected (they don't filter by plan status)

Stripe subscription migration timing

  • Existing Stripe subscriptions continue on their current Stripe price until manually migrated
  • Migration happens per-customer: either at renewal, or via an admin bulk action
  • No automatic migration — this prevents surprise billing changes

Plan swaps with archived plans

  • If a customer on an archived plan wants to upgrade, the swap creates a new subscription on the new plan/cycle price
  • swapSubscription() always uses the target plan's PlanPrice, never the source plan's

Billing Cycle Naming Fix

Existing inconsistency: SyncStripePrices.php (lines 46, 54) and CustomerController.php (line 307) use semi_annually. All other code uses semi_annual. Standardize to semi_annual everywhere as part of this work.

Files to Create

  • database/migrations/XXXX_create_plan_prices_table.php
  • app/Models/PlanPrice.php
  • app/Console/Commands/MigrateVpsPlans.php — artisan command to migrate existing subscriptions/services from old plan IDs to new plan IDs

MigrateVpsPlans Command

  • Maps old plan IDs → new plan IDs using the migration map
  • Updates subscriptions.plan_id for active subscriptions on old plans
  • Updates services.plan_id if services reference plans
  • Logs all changes (old_plan_id → new_plan_id, subscription_id, user_id)
  • Idempotent: skips subscriptions already on new plans
  • Dry-run mode: --dry-run flag to preview changes without applying
  • Does NOT change Stripe subscriptions (those migrate separately)

Files to Modify

  • app/Models/Plan.php — add prices() relationship, priceForCycle() helper
  • database/seeders/PlanSeeder.php — new plans + plan_prices + archive old
  • app/Console/Commands/SyncStripePrices.php — multi-price sync, fix semi_annually→semi_annual
  • app/Http/Controllers/Account/CheckoutController.php — cycle-aware checkout
  • app/Http/Controllers/Admin/CustomerController.php — fix semi_annually→semi_annual (line 307)
  • app/Services/Billing/StripeBillingService.php — cycle-specific price, IPv4 addon, swapSubscription fix
  • app/Services/Billing/PayPalBillingService.php — cycle-specific plan (if PayPal used)
  • resources/ts/Pages/Marketing/Pricing.vue — billing cycle toggle
  • resources/ts/Pages/Checkout/Show.vue — cycle selection, price display
  • resources/ts/Pages/Admin/Plans/Edit.vue — multi-cycle price editing
  • app/Http/Controllers/Marketing/MarketingController.php — eager-load plan_prices
  • resources/ts/types/models.ts — add PlanPrice interface, update Plan interface
  • app/Http/Resources/SubscriptionResource.php — include billing_cycle and plan pricing
  • app/Http/Resources/ServiceResource.php — include plan pricing data

Testing

New Test Files

  • tests/Feature/PlanPriceTest.php — PlanPrice model, Plan::priceForCycle(), seeder verification
  • tests/Feature/MultiCycleCheckoutTest.php — checkout with each billing cycle, IPv4 addon billing
  • tests/Feature/MigrateVpsPlansTest.php — migration command dry-run and execution

Test Cases

  • PlanPrice model: CRUD, unique constraint on (plan_id, billing_cycle), cascade delete
  • Plan::priceForCycle(): returns correct price for each cycle, returns null for invalid cycle
  • PlanSeeder: creates all 8 plans with 4 prices each (32 plan_prices rows)
  • Checkout: monthly/quarterly/semi_annual/annual each use correct stripe_price_id
  • Checkout: IPv4 addon adds correct Stripe line item with quantity
  • Checkout: invalid billing cycle returns validation error
  • SyncStripePrices: creates 4 Stripe prices per plan, stores IDs
  • MigrateVpsPlans: maps old plans to new, skips already-migrated, dry-run works
  • swapSubscription: uses cycle-specific price from target plan

Out of Scope

  • VirtFusion API package creation (manual via VirtFusion admin panel)
  • Bulk Stripe subscription price migration (per-customer at renewal or future admin tool)
  • PayPal multi-cycle support (existing limitation, separate effort)
  • Customer communication/email about pricing changes
  • Mid-subscription IPv4 quantity changes (add/remove IPs after checkout — future feature)