Zack Meacham

Personal project · desktop

← All work

Shipping a Production-Grade Desktop App Solo, End to End

2026 · Shipped · v1.0.2

A Windows desktop companion app for My Singing Monsters, built solo from greenfield to six tagged releases. The interesting part isn't the feature set; it's the reliability engineering underneath: atomic database swaps with rollback, a threat-modeled auto-updater, a two-database design whose user progress survives full content rebuilds, and a self-publishing CI/CD pipeline.

  • Python
  • PySide6 / Qt 6
  • SQLite
  • PyInstaller
  • Inno Setup
  • GitHub Actions

At a glance

Role
Solo author, end to end (greenfield to six releases)
Year
2026
Status
Shipped · v1.0.2 · six tagged releases
Stack
Python 3.10 · PySide6 / Qt 6 · SQLite · PyInstaller · Inno Setup · GitHub Actions
Scope
Windows desktop app · 64 monsters / 76 egg types of content
Tests
505 tests across 34 modules (31 unit, 3 integration) · real in-memory SQLite · pytest-qt
Process
103 commits · 15 single-concern PRs · Conventional Commits
Proof
Atomic SHA-256-verified updater with rollback · two-database stable-identity design · self-publishing CI/CD

Why this mattered

A desktop app that auto-updates its own content is deceptively risky: a botched update can corrupt a user's saved progress or brick startup, and there is no server-side rollback to save you. Most hobby apps either skip auto-update or do it unsafely. This one treats the update path as the highest-risk surface: the content database swaps atomically with automatic rollback, the channel is hardened against a concrete poisoned-manifest threat model, and user progress is decoupled from the content's primary keys so it survives a full rebuild. The bar was production-grade reliability for a solo project, not just feature delivery.

My role and ownership

Solo, end to end. I own the domain logic, the Qt UI, the two-database SQLite design, the in-app updater, the maintainer content pipeline, the installer, and the CI/CD. Every release shipped through a feature-branch and pull-request workflow (15 merged single-concern PRs across 103 commits), including a deliberate over-engineering audit that removed dead code as first-class reviewed work.

Core constraints

  • Auto-updating content with no server-side safety net: a bad update can corrupt saved progress or brick startup, so the update path has to be atomic and self-healing.
  • User progress must survive full content-database rebuilds, even when numeric primary keys are reassigned.
  • The update channel is an attack surface: a poisoned manifest could force an HTTP downgrade, a local-file read, or an attacker-controlled host.
  • Solo maintenance: the build, the tests, and the release gates have to make a broken artifact structurally unable to ship.

Architecture and key decisions

Strictly-layered Qt application core

A pure-Python domain core free of Qt and SQLite, function-style repositories with dependency-injected connections, an AppService(QObject) orchestrator, and a passive UI that reads only frozen ViewModel dataclasses, all on a unidirectional dependency arrow. A Command pattern drives disciplined undo/redo: each command snapshots its own reversal state, and the service re-pushes a command on a failed undo so the in-memory history can never desync from the database.

  • PySide6
  • Command pattern
  • Layered architecture

Two-database design with stable identity

A read-only content.db (reopened through a SQLite URI in mode=ro, so a stray write raises OperationalError) paired with a read-write userstate.db. A slug-based stable-identity system (content_key) decouples user progress from numeric AUTOINCREMENT ids; partial unique indexes enforce uniqueness of populated keys while tolerating pre-backfill placeholders. Post-update reconciliation rebuilds progress by stable key (delete-then-insert) so saved state survives a full content rebuild without orphaning.

  • SQLite
  • Stable identity
  • Atomic migrations

Atomic, threat-modeled auto-updater

The updater streams a new SQLite database in 64 KB chunks under a 64 MB ceiling and trusts it only after a mandatory SHA-256 check plus a PRAGMA integrity_check and an orphan-row schema audit, then swaps it atomically with os.replace() and automatic rollback so content and user state are never left mismatched. The channel is hardened against a poisoned-manifest threat model: HTTPS-only with a host allowlist (blocking HTTP-downgrade, file:// reads, and attacker-host redirects) behind a numeric, non-lexical min-version gate.

  • SHA-256
  • Atomic swap
  • Threat modeling

Maintainer ETL content pipeline

An offline ingest, normalize, semantic-diff, deterministic-build, validate, publish pipeline kept strictly out of the app runtime. A semantic diff classifies content changes into a typed taxonomy, and a 9-gate publish-validation system (integrity, orphan-FK scans, unique-key, JSON-Schema conformance, and a blocking review gate) stops a bad release. It emits a versioned, checksum-stamped artifact contract, the exact manifest the in-app updater verifies before applying.

  • ETL
  • JSON Schema
  • Deterministic builds

Build, release, and CI/CD

A fail-fast build script chains version injection, pytest, content-DB seed, asset and icon generation, bundle verification, PyInstaller, and Inno Setup as checked subprocesses, aborting on the first non-zero exit. A bundle-verification gate cross-validates the seeded content DB against on-disk assets and must exit 0 before any release tag. Three least-privilege GitHub Actions workflows handle PR test gating, tag-driven release builds, and a self-publishing content workflow that wires CI directly into the app's update channel.

  • PyInstaller
  • Inno Setup
  • GitHub Actions

Execution highlights

  • Made SQLite schema migrations atomic (each wrapped in BEGIN/COMMIT with rollback), eliminating a startup-bricking class of partial-apply failures, with the fix landing alongside a dedicated regression test.
  • 505-test suite across 34 modules against real in-memory SQLite, including adversarial updater tests (SHA-256 mismatch, scheme and host allowlist rejection) and a live-HTTP end-to-end harness exercising the full fetch, validate, swap, rollback loop; acceptance tests map traceably to formal SRS criteria.

Impact / current state

Shipped and maintained: six tagged releases from three betas through v1.0.0 to two hardening and maintenance point releases (v1.0.2), each a real git tag with a matching GitHub Release. A dedicated v1.0.1 security-hardening pass made SHA-256 mandatory and capped streaming downloads.

What this demonstrates

End-to-end ownership of a shipped desktop product, reliability engineering as a first-class concern (atomic swaps, rollback, read-only enforcement), threat-model-driven security, and the discipline to treat deletion and gated releases as real engineering, delivered solo.