Skip to content

Upgrading & Versioning

How to take upstream updates into a codebase you have already made your own.

This is a template, not a dependency. You own your copy: there is no package to bump and no framework updating itself underneath you. The catch is that taking an upstream change means doing a merge, and how hard that merge is depends mostly on choices you make in your first week.

Set it up so upgrades stay cheap

Clone rather than download, and keep upstream as a second remote from day one. A ZIP with no shared history turns every future update into a manual diff.

Terminal window
git clone <repository-url> your-saas-app
cd your-saas-app
# rename the upstream remote, then point origin at YOUR repository
git remote rename origin upstream
git remote add origin <your-own-repo-url>
git push -u origin main

Now git fetch upstream brings updates in as real history, and git merge does the work. Keep upstream read-only; never push to it.

Build in the places designed to be extended

Every merge conflict is a line you changed that upstream also changed. The architecture gives you places to add code that upstream doesn’t touch. Use them, and most releases merge cleanly:

  • Plugins are the main one. A feature built as a plugin lives in its own project, registers itself, and adds nav, routes, admin pages, public routes and background sweeps without editing a host file. See Plugin Development.
  • Provider interfaces. Payments, SMS, email, file storage and AI are all interfaces with shipped implementations. Add your own implementation beside them rather than editing theirs.
  • Settings and configuration over code. Feature switches and provider credentials are saved in the app, from its admin pages; per-plugin settings are configuration. A behaviour change made there costs nothing at merge time; the same change made in a host file costs you on every release.
  • New files rather than edited ones. A new service in your own namespace never conflicts. When you must edit a shipped file, keep the edit small and obvious, and don’t reformat the code around it.

The one thing worth doing on day one

Commit your own work in focused commits with real messages. When a merge conflicts eighteen months later, you will need to know why you changed that line, and your commit history is the only thing that can tell you.

Versioning

The VERSION file at the repo root holds a three-part version (1.0.0). It is read in two places:

  • Image tags. Both pipelines run scripts/compute-version.sh to tag images {VERSION}.{build number}, which is immutable and is what a deploy pins, and also move floating {VERSION} and latest tags. To cut a release, bump the file. The pipelines create no git tags.
  • Assembly versions. Directory.Build.props sets every project’s <Version> from it, and that is the host version plugins are checked against (see Plugins after an upgrade).

The second use is why you should keep VERSION in step with upstream rather than restarting it at your own numbering. A host whose VERSION is lower than the release it was merged from refuses plugins built against that release. Take upstream’s value on each merge (git show upstream/main:VERSION), and track your own releases in your merge commit messages or a CHANGELOG.

Performing an upgrade

Terminal window
git fetch upstream
git checkout -b upgrade/<version> # never merge straight into main
git merge upstream/main
# resolve conflicts, then:
dotnet build BlazorBlueprint.sln --configuration Release
dotnet test BlazorBlueprint.sln --configuration Release

Then work through these steps in order:

  1. Diff the configuration. A merge won’t add new keys to an appsettings.json you have edited. Compare each host’s file (Web, InternalApi, BackgroundWorker, DbMigrator) against upstream’s, plus the overlays you use (appsettings.Production.json in the three app hosts, Web/BlazorBlueprint.Web/appsettings.Lite.json). Feature switches are not in these files: they are saved in the platform database and edited at Platform → Platform Settings → Features, and a switch a release adds reads its documented default until you change it (see Architecture).
  2. On PostgreSQL or SQL Server, check the schema. Run scripts/check-migration-drift.sh. If it reports model changes, add a new named migration for them, then run scripts/check-additive-migrations.sh. Never run scripts/regen-migrations.sh once a database has applied your migrations: it deletes the Migrations/ folders and generates a fresh InitialCreate, which databases that applied the old one then fail to migrate. The same applies to what upstream ships: if the merge deletes an InitialCreate your databases have applied and adds a differently-named one, or conflicts inside the Migrations/ folders, keep the migrations your databases have already applied, discard upstream’s replacement, and add a migration for the difference. See Relational Schema Management. The relational test suites skip themselves when no database server is available, so a passing local test run doesn’t prove the schema is right. MongoDB needs no migrations.
  3. Test against a copy of production data, not an empty database. Provisioning and index reconciliation are designed to be safely re-runnable, and an upgrade is when that gets tested for real.
  4. Deploy to staging first, with the DbMigrator running before the app. The shipped pipelines do this, so a migration failure blocks the deploy before the running app is touched. On Lite there is no DbMigrator service: relational tenants migrate when the web host starts, but on MongoDB you must run the DbMigrator image yourself to get new indexes. See Deployment → Migrations under Lite.

Plugins after an upgrade

A plugin is checked against the host version at startup, by two rules:

  • Manifest.MinHostVersion — the oldest host the plugin’s author says it supports.
  • The compiled reference — the highest version of any of the host’s own BlazorBlueprint.* assemblies the plugin was built against, read from the plugin assembly itself. A reference to another plugin’s assembly, or to one the plugin ships beside itself, does not count, so a plugin family versioned on its own is not mistaken for one built against a newer host. This needs nothing from the author.

If the host is older than either, the plugin is not enabled and the host logs an Error naming both versions. A host newer than the plugin is accepted, so an upgrade does not by itself force you to rebuild plugins. Rebuild them anyway when the release notes change a contract they use: a plugin always runs against the host’s copy of the shared assemblies.

When a plugin’s own version changes, run the DbMigrator as well, on every backend. See Plugin Development → Upgrading a plugin.

Two things that must survive every upgrade

The Data Protection key ring. Without it, every encrypted field is unreadable. It lives in the redis_data volume on the full stack and in bb_lite_dp_keys under Lite. Snapshot the right one before upgrading, and never run docker compose down -v, which deletes volumes. Caching:RedisKeyPrefix is part of the key ring’s name, so never change it either — a host moved off the shipped default refuses to start rather than mint an empty ring, but no other change is caught. See Deployment → Backups.

On Lite with PostgreSQL or SQL Server, check your names first. Lite now ships docker-compose.lite.postgres.yml and docker-compose.lite.sqlserver.yml. Earlier releases told you to edit docker-compose.lite.yml in place for a relational backend, with service and volume names of your choosing. The new files name them postgres / bb_lite_postgres_data and sqlserver / bb_lite_sqlserver_data. Before the first up with one of them, either keep using your edited file, or change those two names in it to the ones your data is in (docker volume ls). Otherwise it starts on an empty database and /initial-setup reopens.

On Lite, upgrade in place. Don’t unpack a new version into a new folder, rename the folder, or clone into a differently-named one. Compose prefixes volume names with the folder name, so a new folder can mean a new, empty key-ring volume pointed at your existing database. The Lite compose files pin the project name and every volume name to prevent this, so don’t rename them, and run docker volume ls after upgrading to confirm the stack adopted its existing volumes.

What your licence entitles you to

  • Below the revenue threshold, the free grant covers using the template and taking updates, with the “Powered by” attribution kept in place. Support isn’t included.
  • A Commercial Licence includes every update released in the 12 months after payment clears, plus email support over that period. When that period ends, everything you already have stays yours to use and modify. There is no forced renewal, and what lapses is access to newer releases.

The LICENSE file at the root of the source has the full terms, including the threshold. Pricing is at blazorblueprint.net/download.