uBixCore 0.2 — From Monorepo to Framework, Skeleton, and Runtime
The Problem With Being a Monorepo
When I open-sourced uBix Core it was a monorepo in the literal sense: the framework, the CLI, the coding standard, and the product that used them all lived in one tree. That is a fine way to build a product. It is a poor way to ship a framework, because the only way to use it was to fork the whole thing, product included, and then spend the rest of your life merging upstream.
Laravel and Symfony solved this a long time ago with the same shape: the framework is a package, your project is a thin skeleton that depends on it, and updates arrive through composer update. uBixCore 0.2 is that shape.
What 0.2 Is
Three public repositories under github.com/ubixsys, each doing one job:
| Repository | What it is |
|---|---|
| ubixcore | The framework. Ubix\ namespace, the bin/ubix CLI, typed request and response contracts, the migration runner, the phpcs standard, the PHPStan baseline, the PHPUnit base classes. Published as ubixsys/ubixcore on Composer and @ubixsys/ubixcore on npm. |
| ubixcore-skeleton | The composer create-project template: two thin entry points, one hello-world API, the quality gate wired to the rules that ship in the framework, and nothing from the framework committed in your repo. |
| ubixcore-php | The runtime image: nginx, PHP-FPM, memcache(d), Composer and supervisord on Alpine, multi-arch, unprivileged, listening on 8080. Pulled as ghcr.io/ubixsys/ubixcore-php:8.5. |
A host project owns about a dozen lines of bin/ubix and public/index.php. Everything behind them updates with the package. This week that was proven the pleasant way: the framework’s CLI got a new banner and a renamed test base, and the product picked both up with a composer update and a green pipeline.
The Product Had to Move Out First
The interesting engineering was not the packaging. It was getting a live product out of the framework’s repository without anyone noticing.
Sowing.me now lives in its own private monorepo, built on uBixCore as a dependency, with its own namespace, its own migrations, and its own Kubernetes manifests. The cutover ran one API at a time on dev, then everything at once on staging and production, deploying over the same Deployment and Ingress names so nothing about the cluster changed. Three things bit on the way, and each one became a framework fix:
- A baked
.envhides every piece of config an image depends on. The first pod from the new repo logged to the wrong path, lost sessions and failed database calls, because the old image had carried a.envfile the new one did not. Non-secret settings now live in the Deployment spec. Secrets were never the problem, because uBixVault already supplied them at runtime. - CI proving the migration runner is not the same as migrating a database. The pipeline had always run
migrate:up, against a throwaway database it created for the tests. No tier database had ever been touched. The pipeline now has amigratestage before every deploy: dev migrates dev, staging migrates the shared staging/prod database, and a migration marked destructive is held until a human runs the manual job that acknowledges it and snapshots the tables first. - A public repository needs a public history. After the product code left, the branches were rewritten to a single root commit, with the full history kept on an internal archive branch. Every legacy name and internal hostname was scrubbed first. Lesson worth writing down: grep the tree for anything that looks like a token before every tag, not after.
Secrets: uBixVault First
Every document in the three repositories says the same thing about secrets, on purpose. Pipelines read them from uBixVault with a read-only token scoped to that project’s path, pods resolve database credentials from Vault at startup, and a .env file is a local development convenience that never gets baked into an image. The one CI variable a project needs is the Vault token itself.
What 0.2 Is Not
It is not 1.0, and the version number is honest about that. Today the Composer packages publish to a private registry, so composer create-project ubixsys/ubixcore-skeleton does not yet work for someone off the internet. The Svelte package is an empty shell waiting for its first real component. A handful of framework classes still carry NOT_IMPLEMENTED notes from their monorepo days.
The road from here is one minor version per gap:
| Version | Milestone |
|---|---|
| 0.3 | Packages on Packagist and npmjs, so the install commands on ubixsys.com work for a stranger |
| 0.4 | Skeleton builds anywhere, on the public runtime image, with no private registry in sight |
| 0.5 | No stubs, and at least one real component in the Svelte package |
| 0.6 | A “first app in 30 minutes” tutorial: controller, payload, datatype, repository, migration, test |
| 1.0 | A clean-machine install following only the page, and one product feature shipped dev to prod on the published packages with no framework patch |
That last row is the real definition of done. A framework is stable when its host can ship without touching it.
Try It
The 0.2 preview is readable today at github.com/ubixsys/ubixcore, with the full write-up of how the split was done, step by step, on ubixsys.com/ubixcore. If you want to kick the tires before 0.3 lands on Packagist, the skeleton’s README explains how to point Composer at the GitHub repository directly.