Bonumark Stream v0.7.2 is now available. Most of the work behind it did not start with a new feature I wanted to add. It started when I moved Bonumark into a more serious hosting environment and discovered that some of the assumptions underneath the application were no longer good enough.
v0.6.0 Changed the Product. The Next Problem Was Running It.
The publishing system already worked. What needed work was everything around running, upgrading, verifying, and maintaining it as a real self-hosted application. Bonumark Stream v0.6.0 had already been a major product pass, introducing first-class Profiles, Theme Architecture 2.0, the rebuilt Midnight Ledger default theme, stronger Profile media handling, and cleaner boundaries between application behavior, presentation, link previews, and SEO.
I was happy with where the product itself was going. Then I moved more of my web infrastructure onto a VPS, and the environment around Bonumark changed. On typical shared hosting, it is normal for the PHP process to have enough filesystem access to update application files because the hosting account and web process largely operate inside the same ownership model. I did not want to reproduce that model on my VPS just because it made Bonumark’s existing updater convenient.
I had already separated sites with their own PHP-FPM identities and was trying to keep the server on a least-privilege model. The web process should be able to write the things the application legitimately needs to write. It should not automatically own the application itself.
That gave me a specific requirement: Bonumark needed to work normally without requiring PHP to own or replace Bonumark’s software files. Publishing, media uploads, imports, scheduled tasks, themes, temporary files, content versions, and other runtime behavior still needed to work. I was not willing to “secure” Bonumark by disabling the things Bonumark is supposed to do, and I was not willing to solve the problem by making the application tree broadly writable again.
That decision drove most of the work between v0.6.0 and v0.7.2.
I Separated Runtime Access From Software Ownership
The first thing I had to do was make Bonumark understand the difference between runtime storage and application software. Before this work, several parts of the application had their own ideas about what needed to be writable. Installation had a list. Security diagnostics had another list. Upgrade behavior made additional assumptions. That is manageable until the assumptions stop matching the server.
The runtime directory model was consolidated so Bonumark had one definition of the paths it legitimately needs to use while running. That includes temporary storage, media, content versions, import staging, exports, upgrade storage, application data, and Markdown import paths. Installation, System Check, and security diagnostics now use that same definition.
That mattered because I no longer wanted “Can PHP write the application?” and “Can Bonumark function?” to mean the same thing.
They are not the same question.
A locked-down installation can have a read-only application tree from PHP’s perspective while still giving the web process appropriate access to the runtime directories it needs. Once that distinction was established, it could be applied to the upgrade system.
Bonumark now detects whether the current environment actually supports automatic software replacement. It checks the package-managed files and directories that would need to change instead of assuming an upgrade can happen simply because the application is running. If the environment safely supports a browser-based upgrade, Admin Upgrade can still do it. If it does not, Bonumark reports that as a capability limitation instead of pretending the entire installation is unhealthy.
That was an important product decision for me. Missing an optional convenience feature should not automatically mean Bonumark is broken.
Hosting Capabilities Became Something Bonumark Could Actually Understand
Once I started looking at hosting this way, I found more assumptions worth removing. Bonumark now detects and reports the web server family, cURL availability, ZipArchive availability, GD or Imagick image-processing support, effective upload limits, theme ZIP installation capability, and automatic application-upgrade capability. Those are individual capabilities, not one giant supported-or-unsupported switch.
A host without ZipArchive does not mysteriously become unsupported if Bonumark can otherwise run there. A locked-down theme directory does not require weakening permissions just so the Admin theme installer can write to it. Bonumark can report that theme ZIP installation is unavailable in that environment and point toward a manual deployment path instead.
I also removed fatal assumptions around mbstring. Bonumark can use it when it is available, but affected text operations now have guarded fallbacks rather than turning one missing optional extension into an application failure.
This work changed how I describe the product too. Bonumark is not a “shared-hosting application.” It is a self-hosted application that supports shared hosting.
That is a meaningful distinction.
Nginx Became a Real Deployment Target
Moving Bonumark onto my VPS also meant running it behind Nginx instead of relying on the Apache and LiteSpeed behavior I had been using before. I did not want Nginx support to mean, “Jim figured out some configuration that works on his server.” If I was going to call it supported, Bonumark needed maintained documentation and a reference configuration.
The project now includes Nginx guidance covering Bonumark’s clean routes, private _bonumark_stream protection, CLI-only script protection, Authorization header forwarding, PHP handling, prevention of PHP execution from media paths, and upload sizing. Apache and LiteSpeed remain supported through the included .htaccess rules, which were also hardened so private and hidden-file restrictions behave correctly across modern and legacy authorization directives.
I also added documented manual deployment workflows for Bonumark software and themes. That created a fallback for hosting environments where PHP cannot replace application files and shell access is not available. At that point Bonumark could run in a locked-down environment, but the maintenance workflow still felt more manual than I wanted.
I Built an Installed-Site Verification System
One problem with manual deployment is that copying files is not the same thing as proving an upgrade worked. A non-destructive overlay can leave old package files behind. Version markers can disagree. Files can be missing or modified. A database migration can still be pending even though the new PHP files are already in place.
I did not want “the copy command finished” to become my definition of a successful deployment. That led to scripts/deployment-check.php, a read-only installed-site verification tool that checks version-marker consistency, package-managed file integrity, obsolete package files, required runtime directories, database compatibility, pending migrations, and migration-recovery state.
Some existing diagnostics also needed to change because testing a security boundary should not require weakening that boundary. Bonumark had used a temporary file inside the private application directory to test whether that directory was exposed over HTTP. On a properly locked-down server, the PHP process should not need permission to create arbitrary files there just so a diagnostic can test Nginx. That check now uses a read-only probe against a known private Bonumark file.
The public URL diagnostic was strengthened for the same reason. Instead of reporting success based on an assumption, it now makes a real read-only request to Bonumark’s /api/v1/status route and verifies a Bonumark-specific response.
I wanted the checks to prove something.
Database Compatibility Stopped Being an Assumption
The VPS work also pushed me to define what database support actually means. Bonumark now has explicit compatibility floors: PHP 8.1 or newer, MySQL 8.0 or newer, or MariaDB 10.6 or newer. Fresh installation checks the database family and version before creating the site, and existing installations can see the detected database information through System Check and deployment verification.
The compatibility work also accounts for MariaDB version strings that can present themselves through MySQL compatibility prefixes. That sounds like small plumbing, but it is part of making the application maintainable.
“Uses MySQL” is not a compatibility policy.
I wanted to know what Bonumark actually supports.
Manual VPS Upgrades Still Were Not Good Enough
By v0.6.6, Bonumark had a much better locked-down deployment model. I had documentation, migration tooling, deployment verification, and capability detection. I still did not like the actual software-update workflow.
I could use rsync, SFTP, a hosting control panel, or another application-owner mechanism to replace the files. That worked, but Bonumark already understood the release package, manifests, owner-data boundaries, migrations, backups, rollback behavior, and deployment health. Leaving the most important software replacement operation outside the application’s upgrade logic felt like a split architecture.
The browser upgrader had accumulated useful safety behavior over the life of Bonumark. I did not want a completely separate CLI upgrader with a second interpretation of what an upgrade means. The actual upgrade behavior was moved into a shared upgrade engine used by both Admin Upgrade and the new owner-run CLI workflow.
Both paths now share the same release validation, owner-data preservation, software backup, package-managed file replacement, obsolete-file cleanup, migration locking, migration recovery, rollback behavior, and upgrade-history recording. That became v0.6.7.
The Owner-Run CLI Upgrade Was the Missing Piece
The new workflow is scripts/deploy-update.php. On a server where the operating-system user owns the application and PHP does not, I can run the upgrade as the account that already has legitimate permission to change Bonumark’s software.
I do not give PHP more permission. I do not invoke sudo from the application, install a privileged daemon, use a setuid helper, or create a backdoor around the ownership model I intentionally configured. The deployment user simply performs the deployment.
Before anything changes, the CLI workflow validates the release ZIP and release manifest, checks the target installation, verifies that the current user can replace the required software, identifies pending migrations, and shows the upgrade plan. It refuses accidental root execution unless that is explicitly acknowledged, and when pending database migrations exist it requires confirmation that an external database backup exists before migration work begins.
There is another safeguard around confirmation. After the release package has been validated, Bonumark hashes it and checks it again immediately before installation. That closes the gap where the package could theoretically be replaced between validation and confirmation.
After a successful upgrade, the CLI workflow automatically runs the installed-site deployment check. The command does not simply say the copy succeeded. It verifies the resulting installation before reporting success.
I tested the workflow through the development versions leading to v0.6.8 instead of treating the implementation as proof by itself. That real testing was what convinced me the owner-run upgrade model belonged in Bonumark. v0.6.8 then polished the confirmation behavior so the required UPGRADE confirmation remained explicit without unnecessarily caring about capitalization.
Small change, but the requirement stayed intact: an accidental Enter key should not start replacing application software.
v0.7.0 Was Supposed to Be the Release
Once the hosting portability and upgrade work had stabilized, I consolidated the v0.6.1 through v0.6.8 development builds into a v0.7.0 release candidate under the Hosting Portability & Upgrade Workflow milestone. The runtime product behavior was where I wanted it. Then I started release verification.
That is where this release got more interesting.
I had added a GitHub Actions compatibility matrix so I could stop relying only on my own environment when making database compatibility claims. It covered PHP 8.1 with MySQL 8.0, PHP 8.1 with MariaDB 10.6, PHP 8.3 with MySQL 8.4, and PHP 8.3 with MariaDB 11.4. Each job was supposed to run PHP lint, the clean-package smoke test, database schema and migration testing, and the Remote Posting API database smoke test.
The first real run failed.
Good.
That is what a test is supposed to do when something is wrong.
The First Compatibility Failure Exposed the Test Environment
The package smoke test is intentionally strict. It checks the release manifest and rejects files that do not belong in a Bonumark release package. GitHub Actions had checked out the repository and then run that package test directly in the Git working tree, which meant .git metadata existed inside the directory being tested.
The smoke test correctly said those files were not part of the Bonumark package. The application was not broken. The test environment was wrong.
I changed the compatibility workflow so it builds a clean tracked source tree with git archive and runs the tests against that instead of pretending a Git checkout is the same thing as a release package. During the same audit, I also found that api/v1/stream/posts.php existed in the release ZIP but had accidentally been omitted from the GitHub release branch.
That is exactly the kind of mismatch I want release verification to catch.
I did not quietly replace v0.7.0. My versioning rules say a delivered or meaningfully tested candidate is immutable. The correction became v0.7.1.
Then the Database Test Found Something More Important
With the workflow corrected, the matrix finally reached the database migration test. All four database combinations failed with the same error: Duplicate column name ‘scheduled_at’. That immediately told me this was not a MySQL-versus-MariaDB compatibility problem.
I traced the test model. Bonumark’s current fresh-install schema already contains scheduled_at, which is correct because a fresh v0.7.x installation should start with the current schema. But the database smoke test was then replaying every historical migration on top of that current schema, including an older migration that added scheduled_at.
The test was effectively saying: create today’s database, then pretend it is an old database and create some of today’s columns again.
I could have made the migration ignore the duplicate. That would have made the test green.
It also would have hidden the real problem.
So I rejected that approach.
v0.7.2 Fixed the Test Model Instead of Hiding the Error
The correct model has two different things to prove. A fresh installation should install the current cumulative schema and verify that the resulting database is correct. An upgrade test should begin with an actual historical database state and apply only the migrations that belong after that state.
Those are different workflows.
v0.7.2 separates them. I added a verified historical v0.4.x schema fixture from Bonumark’s supported upgrade line, and the test verifies that fixture against the original Git history so the historical baseline cannot quietly drift.
The database compatibility test can now test a current fresh installation through Bonumark’s actual installer and separately test a historical supported upgrade through migrations 0002 through 0017. That is much closer to what real installations actually do.
Then I ran the matrix again. All four combinations passed. I created the v0.7.2 pull request into main, and GitHub ran the compatibility matrix again against the actual pull request head. All four passed again. After the release was merged into main, GitHub ran the matrix one more time against the final merge commit. All four passed again.
That is the version I released.
What v0.7.2 Actually Brings to Bonumark Stream
There is no dramatic new publishing interface in this release. That is not what I was trying to build. Profiles, Theme Architecture 2.0, Midnight Ledger, Stream Posts, Pages, comments, media, publishing, and the rest of the visible product continue from v0.6.0. The work is underneath them.
Bonumark now has a defined runtime-storage contract instead of scattered filesystem assumptions. It can distinguish normal application writability from permission to replace its own software, and automatic upgrades are treated as a hosting capability instead of a requirement. Locked-down application trees and conventional shared hosting are both supported without pretending they need the same filesystem model.
Nginx now has maintained deployment guidance, manual software and theme deployment have documented fallbacks, installed sites have read-only deployment verification, migration state and obsolete package files can be checked, and the private-path and public-route diagnostics test real boundaries without changing them.
The Admin upgrader and owner-run CLI upgrader also share the same upgrade engine. The CLI path can bootstrap an older supported installation without requiring me to temporarily weaken the application’s ownership model. Database support has explicit floors, optional PHP capabilities are detected individually, and the repository now has a compatibility matrix that has actually been used to find problems rather than existing as a decorative green badge.
The Release Was Tested Like a Release
The final v0.7.2 package went through more than a version-number change. I linted 210 PHP files, ran syntax checks across 7 JavaScript files, parsed 18 JSON files, and passed the package smoke test. The final release manifest contained 302 files with 0 missing files, 0 unexpected files, and 0 hash mismatches.
I also checked Markdown links and scanned the package for configuration files, install locks, SQL dumps, SQLite databases, logs, PEM files, private keys, and token patterns that did not belong in a public package. Then I built the final ZIP, extracted the actual distributable, reran the package smoke test from that extracted copy, and reverified the manifest.
GitHub then tested the code against the four PHP and database combinations. The important part is that those tests did not all start green.
They found things.
The failures exposed assumptions in the workflow and in the database test model. I corrected those assumptions and reran the tests until the thing being tested actually matched the way Bonumark is installed and operated.
That gives the green checks some meaning.
No New Database Migration
Despite the amount of work in this release, v0.7.2 adds no new database migration compared with v0.6.0. The migration set still ends at 0017_profile_photos.php, and the maintained upgrade floor remains v0.4.0.
Installations older than v0.6.0 can still have earlier migrations that need to run, but those use the existing migration ledger and recovery model. The upgrade boundary also continues to preserve owner data, including configuration, database content, media and uploads, settings, runtime data, backups, custom themes, and the other site state Bonumark is supposed to protect.
The point of upgrading Bonumark should be replacing Bonumark’s software, not treating the owner’s site as disposable.
What This Release Proved to Me
v0.6.0 made Bonumark a better publishing product. v0.7.2 makes it a better self-hosted application. Those are different kinds of progress.
This release forced me to think about the product outside the environment where I originally built it. I had to define what PHP actually needs permission to do, what the application owner should control, what an upgrade should prove before it reports success, what “database compatibility” actually means, and how to test historical upgrades without pretending today’s schema is yesterday’s schema.
The VPS did not just give Bonumark another place to run. It exposed assumptions. That is why I am glad I made the move before adding the next major layer to the project.
I could have loosened permissions until the old workflow worked again. Instead, I changed Bonumark so the application fits a better hosting model. I could have kept using manual file overlays forever. Instead, the owner-run upgrade path now uses the same upgrade engine as the Admin interface. I could have changed a migration until a flawed test turned green. Instead, I fixed the test so it represents real fresh installations and real historical upgrades.
Bonumark Stream is still what I intended it to be: a self-hosted microblog CMS for publishing short-form content on a site the owner controls. v0.7.2 does not try to turn it into a social network or a giant publishing platform. It makes the existing idea easier to operate seriously, whether it is running on conventional shared hosting or a locked-down VPS.
That is the work behind v0.7.2.
Now that this foundation is in place, I can move back toward the part of the roadmap I have been waiting to work on: ActivityPub. I wanted the application underneath it to be ready first.
New Here?
Read Next:
- Bonumark Stream v0.6.0 Is Here
- Builder Receipt: Bonumark Hosting Portability
- Why I Built Bonumark Stream