A framework you install, not a repo you fork. The PHP core, the React component library and the whole quality gate arrive through Composer and npm; your project keeps its own namespace and its own git history, and uBixVault holds its secrets.
$ composer create-project ubixsys/ubixcore-skeleton acme $ cd acme && APP_NAME=HelloApi php -S 127.0.0.1:8080 -t public $ curl 127.0.0.1:8080/health # {"status":"ok","app":"HelloApi"} $ bin/ubix code:review # phpcs · phpstan (level max) · phpunit
uBixCore is two things published together from one tree: a framework (ubixsys/ubixcore on Composer, @ubixsys/ubixcore on npm) and a skeleton (ubixsys/ubixcore-skeleton) that composer create-project turns into a working host project. The framework lives in vendor/ and is never committed to your repository; your committed composer.lock is what pins the version every image is built from.
The point of the split is that the tooling ships with the framework, not just the library: the bin/ubix CLI, the Ubix phpcs standard with its custom sniffs, a PHPStan baseline at level max, PHPUnit base classes that enforce the house rules on every class, the migration runner, and the Docker and GitLab CI templates. A new project starts with the whole workflow.
Ubix\ is the framework's namespace; you never write code there. Your project has its own PSR-4 root and the framework finds your controllers, commands and tests through Composer's map. Secrets are read from uBixVault at startup; non-secret configuration is environment on the Deployment. The image ships no .env, on purpose.
// php/Acme/Controller/HelloController.php namespace Acme\Controller; use Ubix\Controller\AbstractController as Controller; final class HelloController extends Controller { public function hello(Request $request, Response $response): Response { return $this->renderJson($response, ['hello' => 'world']); } } // app/AcmeApi/src/Routes.php $app->map(['GET'], '/hello', HelloController::class . ':hello');
$ bin/ubix code:review # same three tools CI runs, same rules from vendor/ $ vendor/bin/phpcs # <rule ref="Ubix"/> — 230+ sniffs $ vendor/bin/phpstan analyse # includes the framework's level-max baseline
$ bin/ubix migrate:diff # live schema vs checked-in reference $ bin/ubix migrate:up # apply in filename order, destructive-change guard $ bin/ubix migrate:status
$ composer update ubixsys/ubixcore $ git add composer.lock && git commit -m "chore: ubixcore 0.33.0" # one-line diff, one MR
vendor/. Nothing of it is committed; the lock file records which version built each image.Ubix\ is reserved for the framework. Controllers, models, data types and commands live under your own root — Acme\, Kitg\ — and the house rules apply to them by family, not by vendor prefix.docs/ci-setup.md lists every variable and secret a first green pipeline needs.bin/ubix finds commands through Composer's PSR-4 map; add your namespace to one array and your commands appear beside the framework's.Ubix phpcs standard with custom sniffs, PHPStan at level max, strict PHPUnit, and a test base that checks every class against the house rules — and proves each concrete class has a test.diff against a reference dump, and a destructive-statement guard with a backup before it applies.migrate:up refuses one labelled for another tier outright. A production clone restored into staging cannot be migrated as staging until somebody relabels it deliberately — the check is the database's own answer, not a variable the pipeline could have set wrong.migrate:diff replays the migrations into a scratch schema and compares that against the live one, normalising the differences a server's own rendering invents — redundant per-column collation, auto-generated JSON checks, a default row format — so what is left is real drift. That is what makes it a blocking CI job instead of a wall of noise.VAULT_APP_KV_PATHS), so a secret two apps need is stored once, not copied; an app with no database sets VAULT_DB_STRATEGY=none to skip the database-credential read (since 0.46). CI reads its secrets with read-only tokens. No .env in an image, ever.Ubix\Service\Realtime: a dependency-free NATS publisher that waits for the server to accept each event, and short-lived signed subject grants in a language-neutral format, so a WebSocket gateway in any language can verify what a browser may subscribe to. Since 0.17.*ServiceInterface, with the vendor in a separate implementing class. Swapping one is a container binding, not a call-site change — and the interface is what a test doubles, so the logic above it is testable without the vendor present.PaymentProviderServiceInterface: checkout, subscriptions, refunds, webhook verification, connected accounts and transfers — plus what the processor actually charged on a settled payment, which no webhook carries. StripePaymentProviderService is the only class that imports the Stripe SDK.MediaStorageServiceInterface: presigned uploads straight from an untrusted browser, server-side uploads for trusted jobs, and signed reads. Keys are minted by the implementation so one caller cannot guess another's — with one opt-out for multi-file artifacts like an HLS package, whose playlists name their segments relatively.IdentityProviderServiceInterface with Google (OIDC, ID-token verification) and, since 0.45, Facebook — its token inspected at the Graph API, checked against your own app id, with appsecret_proof on every call — plus a signed-request parser for Meta's data-deletion callback. Authorization-flow state and return-to validation are their own services. RealtimePublisherServiceInterface over core NATS, with per-subject grants.SafeMarkdownService renders user-written prose and then enforces an allow-list over its output, so the policy — which nodes, which link attributes, which heading levels — is the host's to pass in rather than the renderer's to assume.bin/ubix ci:aiReview reads an MR's diff, asks Gemini with generic review rules plus the host's own conventions, and keeps one advisory note on the MR up to date. Skips green without keys; never blocks a merge. Since 0.38.ci/ubixcore-pipeline.yml is one GitLab pipeline for every uBixCore app — build, test, migrate, deploy, promote, notify, approval and maintenance — that you include pinned to a release tag and tune with UBIX_* variables. Extend it with the hidden .ubix_* jobs, or drop a job with rules: [{ when: never }]. The deploy, promote and notify scripts ship in ci/bin and are fetched at the version in your composer.lock, so apps keep no copies; UBIX_NOTIFY_SCRIPT, UBIX_DEPLOY_SCRIPT and UBIX_PROMOTE_SCRIPT point at your own instead. Promote refuses when it cannot check the batch for destructive migrations; deploy also applies -cronjob*.yaml and waits for each rollout. ci/bin/vault-set-key.sh writes one secret key safely. UBIX_STAGING_VAULT picks which uBixVault the staging branch reads. Since 0.47. Every job and variable, with an example.GITLAB_API_TOKEN is set (since 0.46). Any uBixCore image runs it with APP_NAME=UbixOpsApi; there is also a public image (ghcr.io/ubixsys/ubixcore-ops) and a Helm chart, so you deploy it the way we do. Since 0.42.v* tag publishes the PHP framework, the React library and the skeleton with matching version numbers.@ubixsys/ubixcore: the shared component library, in TypeScript, for React Router apps built on the framework.A developer needs a running app; an operator needs a green pipeline. They are different jobs, so they get different walkthroughs.
Requirements: PHP 8.5+, Composer 2, git. Node 22+ only if you add a React app.
$ composer create-project ubixsys/ubixcore-skeleton acme $ cd acme # thin bin/ubix + public/index.php, a HelloApi app, php/App/ (your PSR-4 root), # phpcs/phpstan/phpunit configs pointing at vendor/, Dockerfile, pipeline, .env.example
$ git init -b dev && git add -A && git commit -m "chore: bootstrap from ubixsys/ubixcore-skeleton" # composer.lock is committed on purpose; vendor/, node_modules/ and .env* are not
$ php bin/ubix list # the framework's commands, plus yours $ APP_NAME=HelloApi php -S 127.0.0.1:8080 -t public $ curl 127.0.0.1:8080/health
# rename the namespace: composer.json autoload App\ -> Acme\, php/App -> php/Acme, # then the five files that say App\ (composer dump-autoload afterwards) $ bin/ubix code:review # green before your first commit
Assumed stack: GitLab with its container and package registries, a shell runner with Docker buildx and kubeconfigs, k3s namespaces per tier (ws-<env> for APIs, live-<env> for web), one uBixVault per tier with Kubernetes auth. Substitute your hosts; the shape is the same. Every step is spelled out in the skeleton's docs/ci-setup.md.
secret/<project>/composer, or add your project to the framework's job-token allowlist. The auth enters the build as a BuildKit secret, never a layer.UBIXVAULT_CI_TOKEN_DEV, UBIXVAULT_CI_TOKEN_PROD, GITLAB_PROMOTE_TOKEN.bin/vault-ci-setup.sh <env> <project>, admin token in the environment. It creates the read-only policy, writes test-db, composer and discord, and prints the CI token. Pod credentials live at secret/<app>/<env>/db, read through VAULT_ADDR + VAULT_K8S_ROLE.regcred, the wildcard-TLS label, and the non-secret runtime config (MEMCACHE_SERVERS, database host/port/name, log and template paths) as env on each Deployment.dev and read the first failure against the list at the end of docs/ci-setup.md; each maps to a step above.Lesson learned moving a real product onto this: a baked .env hides every setting an image depends on. Put secrets in uBixVault and the rest on the Deployment, and the image can be promoted between tiers unchanged.
What you get, where it goes, and what you type.
| Package | Registry | Notes |
|---|---|---|
| ubixsys/ubixcore | Composer | The PHP framework: Ubix\ namespace, CLI, standard, test bases, migrations. Source on GitHub. |
| ubixsys/ubixcore-skeleton | Composer | The create-project template. Source on GitHub. |
| @ubixsys/ubixcore | npm | The React 19 + TypeScript component library for React Router apps. |
| ghcr.io/ubixsys/ubixcore-php | container | The PHP runtime image: nginx + PHP-FPM + memcache(d) on Alpine, multi-arch, tags 8.5 and 8.4. Source on GitHub. |
| Path | Owner | Notes |
|---|---|---|
| composer.json / .lock | you | Requires ubixsys/ubixcore; the lock pins the framework your images are built from. |
| bin/ubix · public/index.php | you | Thin entry points that call Ubix\Bootstrap; the only values in them are yours (command namespaces, app name). |
| app/<App>/src/ | you | Dependencies.php (PHP-DI), Middleware.php, Routes.php, plus the app's Kubernetes manifests. One folder per deployable app; APP_NAME selects it per image. |
| php/<Vendor>/ | you | Your PSR-4 root. Families (Controller, Model, Repository, DataType…) get the house rules regardless of the prefix. |
| phpcs.xml · phpstan.neon · phpunit.xml | you | Point at the rules that ship in vendor/ubixsys/ubixcore; add your own overrides below them. |
| vendor/ubixsys/ubixcore | framework | Never committed. Upgrade with composer update ubixsys/ubixcore. |
| Command | Group | Notes |
|---|---|---|
| code:review | Code | phpcs, phpstan and phpunit as one gate, the same locally, in the pre-push hook and in CI. Also code:commit, code:merge, code:loc. |
| migrate:up · status · diff · reconcile | Migrate | Apply pending migrations, list state, diff the live schema against the reference dump, or record one as applied without running it. |
| app:run · app:build · app:deploy | App | Run an app locally, build its image, apply its manifests. |
| database:* | Database | Schema reset and bootstrap helpers for local and test databases. |
| cron:* | Cron | Scheduled jobs, loaded only when a cron command is invoked. |
| ci:aiReview | Ci | Advisory LLM review of a merge request, for an MR pipeline job: the diff goes to Gemini with the framework's review rules plus your own conventions file, and the answer lands as one MR note, replaced on each push. Never a gate. Since 0.38. |
| ci:requireApproval | Ci | Fails an MR pipeline until the merge request has a human sign-off (Approve or a thumbs-up); with Pipelines must succeed on, every merge needs one. Works on any GitLab tier, allows named owners to sign off on their own MRs (and logs it), and fails open on an outage. Since 0.40. |
| <yours> | any | Extend Ubix\Console\Command\AbstractCommand under your namespace; bin/ubix discovers it. |
Nothing in vendor/ knows where your project is or what it is called. The bootstrap tells it, once.
Your bin/ubix and public/index.php are a dozen lines each: they call Ubix\Bootstrap\environment(), then console() or http(). The bootstrap exports the project root, loads secrets from uBixVault, and builds the Symfony Console or Slim app from your app/<App>/src files.
Inside the framework, one service resolves every path from that root, so nothing computes locations from its own file. Commands are discovered through Composer's PSR-4 map, which is why a host namespace works with a one-line change.
The coding standard, PHPStan baseline and PHPUnit base classes are part of the package. A host's phpcs.xml is <rule ref="Ubix"/> plus its file list; the test base decides which rules apply to a class from its family segment, so a Kitg\Controller\Api\X is held to the same controller rules as anything in Ubix\Controller.