Music Store Management Platform

Self-Hosted Windows Installer — PowerShell Design

Version 1.0  |  Phase 1: PowerShell  |  Phase 2: Inno Setup .exe wrapper



# 1. Overview

The Windows self-hosted installer is delivered in two phases. Phase 1 (current) uses a PowerShell script — appropriate for beta customers and IT-managed deployments. Phase 2 wraps the same PowerShell logic in an Inno Setup .exe for a consumer-grade double-click experience. No logic changes are required between phases — only packaging changes.



Phase

Delivery

Target

Phase 1

install.ps1 script

Beta customers, IT-managed stores, technically capable staff

Phase 2

setup.exe (Inno Setup)

All stores — double-click consumer experience, no technical knowledge required



# 2. Installation Phase Overview

The installer proceeds through numbered phases. Phase state is saved to disk so installation can resume after a required reboot. Each phase is idempotent — safe to re-run if interrupted.



Phase

Name

Actions

1

collect-config

Prompt for license key, store name, admin email, admin password. Validate license against API. Save state.

2

enable-virt

Check Windows edition. Enable Hyper-V (Pro/Enterprise) or WSL2 (Home). Reboot if required — resume automatically after.

3

install-docker

Check if Docker already installed. Download and install Docker Desktop silently if not. Wait for daemon.

4

create-dirs

Create C:\Platform directory structure. Write .env, docker-compose.yml, nginx.conf, license.lic.

5

pull-images

Authenticate to private registry using license token. Pull platform Docker images.

6

start-services

docker compose up -d. Wait for API health check to pass.

7

register-service

Install NSSM. Register PlatformService Windows service for auto-start on boot.

8

complete

Clean up state file. Open browser to localhost. Show completion message.



# 3. Hyper-V Restart Handling

Enabling Hyper-V requires a kernel-level change that cannot take effect without a reboot. This is unavoidable on machines where Hyper-V is not already enabled. The installer handles this transparently — the store owner reboots once and installation continues automatically after login.



## 3.1 Detection Flow

Check Hyper-V state:  Already enabled  → no reboot needed, continue to Docker phase  Disabled         → enable, check if reboot required    RestartNeeded = false  → continue immediately (rare)    RestartNeeded = true   → save state, schedule resume, rebootWindows Home — check WSL2 instead:  Already enabled  → no reboot needed  Disabled         → enable WSL2 + VirtualMachinePlatform    Both features require reboot → same resume pattern



## 3.2 Resume-After-Reboot Mechanism

Windows registry Run key schedules the script to execute automatically after the next login. The script detects the resume flag, skips completed phases, and continues from where it left off.



On reboot needed:  1. Save install state to C:\Platform\install-state.json     { phase: 'install-docker', licenseKey: '...', storeName: '...' }  2. Write registry run key:     HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run     PlatformInstallResume =       powershell -ExecutionPolicy Bypass         -File 'C:\Platform\install.ps1' -Resume  3. Prompt user to restart:     'A restart is required. Press Enter to restart now.'After reboot (auto-runs on login):  1. Script starts with -Resume flag  2. Reads install-state.json  3. Removes registry run key  4. Continues from saved phase  5. User sees: 'Resuming installation after restart...'



## 3.3 State File Format

C:\Platform\install-state.json{  "phase": "install-docker",  "licenseKey": "LIC-2024-00142-XXXX",  "registryToken": "eyJ...",  "storeName": "Springfield Music Co.",  "adminEmail": "admin@springfieldmusic.com",  "modules": ["CORE", "MOD-RENTALS", "MOD-LESSONS"],  "savedAt": "2024-09-01T10:30:00Z"}Note: admin password NOT saved to state file.If resuming after reboot, password is re-prompted.State file deleted on successful completion.



## 3.4 User Experience During Restart

============================================  Platform Music Store Installation============================================Enter your license key: LIC-2024-00142-XXXXValidating license... OKModules: Rentals, Lessons, Repairs, AccountingEnter your store name: Springfield Music Co.Enter admin email: admin@springfieldmusic.comEnter admin password: ********Checking virtualization...Enabling Hyper-V...============================================  A restart is required to enable Hyper-V.  Installation will continue automatically  after your computer restarts.  You do not need to run this script again.  Press Enter to restart now, or close  this window to restart manually later.============================================[User presses Enter — machine reboots][Script runs automatically after login]============================================  Resuming installation after restart...============================================Checking Docker...Downloading Docker Desktop...Installing Docker Desktop......



# 4. Script Structure

## 4.1 Script Parameters and Entry Point

#Requires -RunAsAdministrator#Requires -Version 5.1$ErrorActionPreference = 'Stop'param(  [switch]$Resume,  [switch]$Uninstall)# ── Constants ───────────────────────────────────$INSTALL_DIR    = 'C:\Platform'$STATE_FILE     = '$INSTALL_DIR\install-state.json'$LICENSE_API    = 'https://license.platform.com'$REGISTRY_URL   = 'registry.platform.com'$RESUME_KEY     = 'PlatformInstallResume'$SERVICE_NAME   = 'PlatformService'$NSSM_URL       = 'https://nssm.cc/release/nssm-2.24.zip'$DOCKER_URL     = 'https://desktop.docker.com/win/main/amd64/...'# ── Entry point ─────────────────────────────────if ($Uninstall) { Invoke-Uninstall; exit }$currentPhase = 'collect-config'if ($Resume -or (Test-Path $STATE_FILE)) {  $state = Get-Content $STATE_FILE | ConvertFrom-Json  $currentPhase = $state.phase  Remove-ResumeKey  Write-Banner 'Resuming installation after restart...'}Invoke-InstallPhases -StartFrom $currentPhase



## 4.2 Phase Execution Pattern

function Invoke-InstallPhases($StartFrom) {  $phases = @(    'collect-config',    'enable-virt',    'install-docker',    'create-dirs',    'pull-images',    'start-services',    'register-service',    'complete'  )  $active = $false  foreach ($phase in $phases) {    if ($phase -eq $StartFrom) { $active = $true }    if (-not $active) { continue }    Write-Host ''    Write-Host "--- Phase: $phase ---"    Save-State $phase  # save before running    switch ($phase) {      'collect-config'    { Invoke-CollectConfig }      'enable-virt'       { Invoke-EnableVirtualization }      'install-docker'    { Invoke-InstallDocker }      'create-dirs'       { Invoke-CreateDirectories }      'pull-images'       { Invoke-PullImages }      'start-services'    { Invoke-StartServices }      'register-service'  { Invoke-RegisterService }      'complete'          { Invoke-Complete }    }  }}



## 4.3 Virtualization Detection

function Invoke-EnableVirtualization {  $edition = (Get-WindowsEdition -Online).Edition  Write-Host "Windows edition: $edition"  if ($edition -match 'Pro|Enterprise|Education|Server') {    Invoke-EnableHyperV  } else {    Invoke-EnableWSL2  }}function Invoke-EnableHyperV {  $feature = Get-WindowsOptionalFeature `    -Online -FeatureName Microsoft-Hyper-V  if ($feature.State -eq 'Enabled') {    Write-Host 'Hyper-V already enabled — no restart needed.'    return  }  Write-Host 'Enabling Hyper-V...'  $result = Enable-WindowsOptionalFeature `    -Online -FeatureName Microsoft-Hyper-V `    -All -NoRestart  if ($result.RestartNeeded) {    Invoke-ScheduleResumeAndReboot  }}function Invoke-EnableWSL2 {  $wsl = Get-WindowsOptionalFeature `    -Online -FeatureName Microsoft-Windows-Subsystem-Linux  $vmp = Get-WindowsOptionalFeature `    -Online -FeatureName VirtualMachinePlatform  if ($wsl.State -eq 'Enabled' -and $vmp.State -eq 'Enabled') {    Write-Host 'WSL2 already enabled — no restart needed.'    return  }  Write-Host 'Enabling WSL2...'  Enable-WindowsOptionalFeature `    -Online -FeatureName Microsoft-Windows-Subsystem-Linux `    -NoRestart | Out-Null  Enable-WindowsOptionalFeature `    -Online -FeatureName VirtualMachinePlatform `    -NoRestart | Out-Null  Invoke-ScheduleResumeAndReboot}



## 4.4 Resume Schedule and Reboot

function Invoke-ScheduleResumeAndReboot {  $scriptPath = $MyInvocation.ScriptName  # Schedule resume after reboot  $cmd = "powershell -ExecutionPolicy Bypass" +         " -WindowStyle Normal" +         " -File '$scriptPath' -Resume"  New-ItemProperty `    -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' `    -Name $RESUME_KEY `    -Value $cmd `    -Force | Out-Null  Write-Host ''  Write-Host '============================================'  Write-Host '  A restart is required to enable'  Write-Host '  virtualization for Docker.'  Write-Host ''  Write-Host '  Installation will continue automatically'  Write-Host '  after your computer restarts.'  Write-Host '  You do not need to run this script again.'  Write-Host '============================================'  Write-Host ''  $confirm = Read-Host 'Press Enter to restart now'  Restart-Computer -Force  exit  # Won't reach here but good practice}function Remove-ResumeKey {  Remove-ItemProperty `    -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' `    -Name $RESUME_KEY `    -ErrorAction SilentlyContinue}



## 4.5 Docker Installation

function Invoke-InstallDocker {  if (Get-Command docker -ErrorAction SilentlyContinue) {    Write-Host 'Docker already installed.'    Invoke-WaitForDockerDaemon    return  }  Write-Host 'Downloading Docker Desktop...'  $installer = '$env:TEMP\DockerDesktopInstaller.exe'  Invoke-WebRequest -Uri $DOCKER_URL -OutFile $installer  Write-Host 'Installing Docker Desktop (2-5 minutes)...'  Start-Process $installer `    -ArgumentList 'install --quiet --accept-license' `    -Wait  # Refresh PATH  $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine')  Invoke-WaitForDockerDaemon}function Invoke-WaitForDockerDaemon {  Write-Host 'Waiting for Docker daemon...'  $timeout = 90  $elapsed = 0  while ($elapsed -lt $timeout) {    try {      docker info 2>&1 | Out-Null      Write-Host 'Docker is running.'      return    } catch { }    Start-Sleep -Seconds 3    $elapsed += 3    Write-Host "  Still waiting... ($elapsed/$timeout sec)"  }  throw 'Docker did not start. Please restart and re-run.'}



## 4.6 Service Registration (NSSM)

function Invoke-RegisterService {  Write-Host 'Installing service manager (NSSM)...'  # Download NSSM if not present  $nssm = '$INSTALL_DIR\nssm.exe'  if (-not (Test-Path $nssm)) {    $zip = '$env:TEMP\nssm.zip'    Invoke-WebRequest -Uri $NSSM_URL -OutFile $zip    Expand-Archive $zip '$env:TEMP\nssm' -Force    Copy-Item '$env:TEMP\nssm\nssm-2.24\win64\nssm.exe' $nssm  }  # Remove existing service if present  & $nssm stop $SERVICE_NAME 2>&1 | Out-Null  & $nssm remove $SERVICE_NAME confirm 2>&1 | Out-Null  # Docker executable path  $docker = 'C:\Program Files\Docker\Docker\resources\bin\docker.exe'  # Register service  & $nssm install $SERVICE_NAME $docker `    'compose --project-directory $INSTALL_DIR up'  & $nssm set $SERVICE_NAME Start SERVICE_AUTO_START  & $nssm set $SERVICE_NAME AppDirectory $INSTALL_DIR  & $nssm set $SERVICE_NAME AppStdout '$INSTALL_DIR\logs\service.log'  & $nssm set $SERVICE_NAME AppStderr '$INSTALL_DIR\logs\service-error.log'  & $nssm set $SERVICE_NAME AppRotateFiles 1  & $nssm set $SERVICE_NAME AppRotateBytes 10485760  # 10MB  Start-Service $SERVICE_NAME  Write-Host 'Service registered and started.'}



# 5. Uninstall Script

The same script handles uninstallation via the -Uninstall flag. Clean removal for beta testing and troubleshooting.



function Invoke-Uninstall {  Write-Host 'Uninstalling Platform...'  # Stop and remove service  $nssm = '$INSTALL_DIR\nssm.exe'  if (Test-Path $nssm) {    & $nssm stop $SERVICE_NAME 2>&1 | Out-Null    & $nssm remove $SERVICE_NAME confirm 2>&1 | Out-Null  }  # Stop and remove containers  if (Get-Command docker -ErrorAction SilentlyContinue) {    Set-Location $INSTALL_DIR -ErrorAction SilentlyContinue    docker compose down -v 2>&1 | Out-Null    docker image rm registry.platform.com/platform:latest 2>&1 | Out-Null  }  # Remove resume registry key if present  Remove-ResumeKey  # Ask about data  $keepData = Read-Host 'Keep your store data? (y/n)'  if ($keepData -eq 'n') {    Remove-Item -Recurse -Force $INSTALL_DIR -ErrorAction SilentlyContinue    Write-Host 'All data removed.'  } else {    # Remove everything except data and backups    Remove-Item '$INSTALL_DIR\*.yml' -ErrorAction SilentlyContinue    Remove-Item '$INSTALL_DIR\*.conf' -ErrorAction SilentlyContinue    Remove-Item '$INSTALL_DIR\*.env' -ErrorAction SilentlyContinue    Remove-Item '$INSTALL_DIR\*.exe' -ErrorAction SilentlyContinue    Write-Host 'Platform removed. Data preserved in $INSTALL_DIR\data'  }  Write-Host 'Uninstall complete.'}



# 6. Error Handling

All phases run inside a try-catch. Failures show a clear message with context and a support contact. State is preserved so the install can be retried from the failed phase without starting over.



try {  Invoke-InstallPhases -StartFrom $currentPhase} catch {  Write-Host ''  Write-Host '============================================'  Write-Host '  Installation failed.'  Write-Host ''  Write-Host "  Error: $($_.Exception.Message)"  Write-Host "  Phase: $currentPhase"  Write-Host ''  Write-Host '  Your progress has been saved.'  Write-Host '  Re-run this script to retry from'  Write-Host '  where it failed.'  Write-Host ''  Write-Host '  Support: support@platform.com'  Write-Host '  Include the error above in your email.'  Write-Host '============================================'  exit 1}



# 7. Phase 2 — Inno Setup .exe Wrapper

Once the platform is stable and beta is complete, the PowerShell script is wrapped in an Inno Setup installer. No logic changes to the PowerShell script are required — Inno Setup is purely a packaging and UI layer on top.



## 7.1 What Inno Setup Adds

- Professional installer UI — welcome screen, license agreement, progress bar

- Single signed .exe download — no script to right-click and run

- UAC elevation prompt handled automatically

- Code signed binary — no SmartScreen warning

- Windows Add/Remove Programs entry

- Desktop shortcut creation

- Built-in restart handling — Inno Setup has native resume-after-reboot support

- Uninstaller registered in Windows



## 7.2 Inno Setup Wraps the PowerShell Script

; installer.iss (Inno Setup script)[Setup]AppName=Platform Music StoreAppVersion=1.0DefaultDirName={autopf}\PlatformOutputBaseFilename=PlatformSetupSignTool=signtool[Files]; Bundle all dependenciesSource: install.ps1; DestDir: {tmp}Source: DockerDesktopInstaller.exe; DestDir: {tmp}Source: nssm.exe; DestDir: {app}Source: docker-compose.yml; DestDir: {app}Source: nginx.conf; DestDir: {app}[Run]; Execute PowerShell script silentlyFilename: powershell.exe;  Parameters: -ExecutionPolicy Bypass -File '{tmp}\install.ps1'  -DockerInstaller '{tmp}\DockerDesktopInstaller.exe'  -InstallDir '{app}'  StatusMsg: Installing Platform...  Flags: runhidden waituntilterminated



## 7.3 Code Signing Requirements

Certificate Type

Cost/yr

Notes

Standard OV certificate

~$200-400

Removes unknown publisher warning. SmartScreen trust builds over time with installs.

EV (Extended Validation)

~$400-700

Instant SmartScreen trust — no reputation period. Requires hardware token (YubiKey). Recommended for commercial release.



# 8. Distribution Security

## 8.1 Source Code Protection

Python source code is compiled to a native binary using Nuitka before being packaged into the Docker image. The Docker image is distributed via a private registry — customers receive a container with a compiled binary, not readable Python source.



Build pipeline (GitHub Actions):  1. Run tests  2. Nuitka compile:       python -m nuitka --standalone --onefile main.py       Output: single compiled binary  3. Docker build:       FROM ubuntu:24.04       COPY main /app/main     # compiled binary only       CMD ["/app/main"]       # no Python, no .py files  4. Push to AWS ECR:       registry.platform.com/platform:1.6.0       registry.platform.com/platform:latest  5. Update version registry API



## 8.2 Registry Access Control

The private Docker registry requires authentication. License validation returns a time-limited AWS ECR pull token scoped to read-only access on the platform image only.



License validation response:{  valid: true,  modules: ['CORE', 'MOD-RENTALS', ...],  registry_token: 'eyJ...', // ECR token, 12hr expiry  registry_url: 'registry.platform.com',  license_json: '{ signed license file... }'}ECR token properties:  - Read-only (pull only, cannot push)  - Scoped to platform image only  - 12 hour expiry  - Refreshed automatically by tray app  - Even if extracted from config, limited blast radius



## 8.3 License Enforcement in Binary

The Ed25519 public key is embedded in the compiled binary at build time. License verification is compiled code — there is no license.py file to edit or bypass. The binary verifies the license file signature on every startup and checks module permissions on every licensed API route.



# 9. Post-Install Management

Phase 1 (PowerShell) customers manage the platform via command line. Phase 2 customers get the tray app. Both approaches documented below.



## 9.1 Phase 1 — Command Line Management

# All commands run from C:\Platformcd C:\Platform# Check statusdocker compose ps# View logsdocker compose logs --tail=50docker compose logs api --follow# Restartdocker compose restart# Stopdocker compose down# Startdocker compose up -d# Updatedocker compose pulldocker compose up -d# Backup nowdocker compose exec postgres pg_dump -U platform platform  > backups\backup-$(Get-Date -Format 'yyyy-MM-dd').sql



## 9.2 Phase 2 — Tray App

The system tray app (built in Go for minimal footprint) provides a point-and-click interface for all common management tasks. Store owner never needs a terminal.



Right-click tray icon:  ● Platform Manager  v1.6.0  ──────────────────────────  Open Platform          → browser to localhost  ──────────────────────────  Status    ✓ API       Running    ✓ Database  Running    ✓ Cache     Running  ──────────────────────────  🔵 Update Available (1.7.0)    View Changelog    Install Update  ──────────────────────────  Backup Now  View Last Backup: Today 3:00am ✓  ──────────────────────────  Restart Services  View Logs  ──────────────────────────  Stop Platform