Personal project · desktop
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.