uBix Core — Migrations, Feature Flags, and Quality Gates That Bite
Picking Up Where the Launch Left Off
When I open-sourced uBix Core I made a specific claim: a monorepo framework lives or dies on whether shared code stays shared instead of drifting into a pile of copy-pasted helpers. I argued the answer was structural — repository pattern, strict data types, PHP-DI wiring, PHPStan at max — and pointed at Sowing.me as the proof it holds under real traffic.
That was the architecture. What I’ve spent the months since building is the machinery around the architecture — the parts nobody puts in a launch post because they’re not exciting, but which are the actual difference between a framework you can maintain and one that quietly rots. Here’s what landed.
Schema Migrations That Detect Drift
A framework that touches a database needs a migration story, and “run these SQL files in order” is not one. uBix Core now ships a real migration framework driven from the CLI:
bin/ubix migrate:up # apply pending migrations
bin/ubix migrate:status # what's applied, what's pending
bin/ubix migrate:diff # diff the live schema against the migrations
bin/ubix migrate:reconcile # bring the ledger back in line with reality
The two that matter most are diff and reconcile. Migrations rot the moment someone hotfixes a column directly in a database — the files say one thing, the live schema says another, and you find out during the next deploy. diff surfaces that drift on demand, and reconcile closes the gap deliberately instead of by surprise. It’s the same “keep-in-sync, don’t push-once” instinct I keep coming back to across projects.
Feature Flags With an Audit Trail
Shipping to a live membership platform means you can’t gate every change behind a deploy. uBix Core grew a feature-flag layer, again CLI-first:
bin/ubix flag:set payouts.weekly on
bin/ubix flag:get payouts.weekly
bin/ubix flag:list
bin/ubix flag:audit
flag:audit is the part I care about — every flip is recorded, so “who turned this on and when” is a query, not an archaeology dig through chat history. The flags run through the same strict data-type layer as the rest of the domain, so a flag value is a typed thing, not a stringly-typed maybe.
A Code-Review Gate That Actually Blocks You
This is the change I’m most opinionated about. uBix Core has an in-house ubix code:review command, and it went from 3 checks to 11 — a lean pass plus OpenAPI contract validation among them. More importantly, it’s now wired into a pre-push git hook: you cannot push to a dev branch if the review fails.
That inverts the usual order. Most projects run quality checks in CI, after the bad code is already shared and someone’s waiting on a red pipeline. Gating at push means the junk never leaves your machine. It’s paired with a JS-side toolchain — spell-checking (cspell), dead-code detection (knip), plus the usual ESLint/Prettier/Rector/PHPStan — and a set of repo-local .claude skills for the review, deploy-manifest, and extraction workflows, so the same standards apply whether a human or an assistant is writing the code.
Is a hard pre-push gate occasionally annoying? Yes. That’s the point. The annoyance is front-loaded to the one person who can still cheaply fix it.
An Eight-Stage Pipeline and Real Environments
The deploy story matured from “build an image” into an eight-stage GitLab pipeline with dedicated CD scripts and per-environment build definitions — separate Dockerfiles for dev, staging, sandbox, main, and test, on both the PHP and Node sides. Each environment gets the image it should, and the promotion path between them is explicit rather than a pile of manual kubectl commands. For a monorepo where one repo feeds several apps, that separation is what keeps a change to one service from quietly rebuilding all of them.
The Product Kept Proving It
None of the above is theoretical, because it’s all in service of Sowing.me shipping real features on top of the framework: a full memberships system, a creator mode with an expandable role switcher, a centralized theme system (light / dark / system), registration hardened with email verification tokens and proper first/last-name handling, and the unglamorous production grind — CORS matching, HTTPS termination for the API host, Memcache, and structured logging. The frontend settled on SvelteKit 2 with Svelte 5 and Tailwind, sharing nothing with the PHP layer but HTTP contracts, exactly as designed.
That’s the whole thesis, still holding: the framework and the product are the same commit history. Every feature Sowing.me needs is a test of whether uBix Core’s abstractions were real or convenient fictions — and the fictions get found and fixed.
Where It’s Going
The next frontier is making the migration and flag tooling as boringly reliable as the rest of the stack, and continuing to widen the review gate’s coverage. The source is at github.com/cwolsen7905/uBixCore and the living proof is at sowing.me. As always: the code and the product are the same thing.