← All projects
Application framework + tooling

uBixCore

Active PHP 8.5+ · React 19 · Slim 4 · PHP-DI

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.

Packages
ubixsys/ubixcore · @ubixsys/ubixcore · ubixsys/ubixcore-skeleton
License
BSD-3-Clause
Shape
Framework + skeleton (Laravel / Symfony model)
Secrets
Resolved from uBixVault
Synopsis
shell
$ 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
Description

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.

Examples
an endpoint in your namespace
// 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');
the quality gate
$ 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
schema migrations
$ 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
upgrade the framework
$ composer update ubixsys/ubixcore
$ git add composer.lock && git commit -m "chore: ubixcore 0.33.0"   # one-line diff, one MR
Capabilities
Install, don't fork
The framework is a Composer dependency in vendor/. Nothing of it is committed; the lock file records which version built each image.
Your namespace
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.
Tooling ships with it
CLI, coding standard, PHPStan baseline, PHPUnit base classes, migration runner, Dockerfiles and pipeline all arrive with the package. docs/ci-setup.md lists every variable and secret a first green pipeline needs.
Command-discovering CLI
bin/ubix finds commands through Composer's PSR-4 map; add your namespace to one array and your commands appear beside the framework's.
Machine-enforced standard
The 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.
Typed contracts
Interface-bound repositories over DTO options, custom data types for domain values, controllers speaking in typed payloads — Slim 4 and PHP-DI underneath, Latte for templates.
Versioned migrations
Timestamped SQL, a central tracker, diff against a reference dump, and a destructive-statement guard with a backup before it applies.
The database says which tier it is
Every database carries its own environment label, and 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.
Schema drift you can gate on
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.
PII reads are logged
An append-only audit writer for the reads that need one — who looked at whose billing or payout detail, and when. A host records against it; nothing in the framework can delete from it.
uBixVault first
Database credentials and the app's own secrets are resolved from uBixVault at boot over Kubernetes auth. An app can read several KV paths (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.
Real-time publishing
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.
Every vendor behind an interface
Payments, object storage, sign-in, real-time delivery, email, subprocesses and SQL each sit behind a *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.
Payments, not Stripe
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.
Object storage with two trust boundaries
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.
Sign-in and real-time
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.
Sanitised Markdown
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.
AI review in CI
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.
Shared CI pipeline template
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.
uBixOps
An operations app that ships inside the framework: approve a merge request and its sign-off job re-runs by itself — on an Approve or a thumbs-up, and a new repo needs only the webhook once a fallback 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.
One tag, three packages
A single v* tag publishes the PHP framework, the React library and the skeleton with matching version numbers.
React 19 library
@ubixsys/ubixcore: the shared component library, in TypeScript, for React Router apps built on the framework.
Installation

Two scenarios: the first ten minutes, and the first afternoon.

A developer needs a running app; an operator needs a green pipeline. They are different jobs, so they get different walkthroughs.

Scenario 1 · developer quickstart

Requirements: PHP 8.5+, Composer 2, git. Node 22+ only if you add a React app.

1 · create
$ 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
2 · git
$ 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
3 · run
$ 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
4 · make it yours
# 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
Scenario 2 · self-hosted GitLab + k3s + uBixVault

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.

  1. Create the project from the skeleton and push it. The pipeline is included: build → lint-and-test → deploy → promote → notify.
  2. Registry access. The image installs the framework at build time. Either store a read-only deploy token in uBixVault at 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.
  3. CI variables, masked: UBIXVAULT_CI_TOKEN_DEV, UBIXVAULT_CI_TOKEN_PROD, GITLAB_PROMOTE_TOKEN.
  4. Seed uBixVault with 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.
  5. Namespaces: 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.
  6. Push to 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.

Reference

Packages, layout, commands.

What you get, where it goes, and what you type.

Packages · one tag publishes all three
PackageRegistryNotes
ubixsys/ubixcoreComposerThe PHP framework: Ubix\ namespace, CLI, standard, test bases, migrations. Source on GitHub.
ubixsys/ubixcore-skeletonComposerThe create-project template. Source on GitHub.
@ubixsys/ubixcorenpmThe React 19 + TypeScript component library for React Router apps.
ghcr.io/ubixsys/ubixcore-phpcontainerThe PHP runtime image: nginx + PHP-FPM + memcache(d) on Alpine, multi-arch, tags 8.5 and 8.4. Source on GitHub.
Host project layout · what the skeleton gives you
PathOwnerNotes
composer.json / .lockyouRequires ubixsys/ubixcore; the lock pins the framework your images are built from.
bin/ubix · public/index.phpyouThin entry points that call Ubix\Bootstrap; the only values in them are yours (command namespaces, app name).
app/<App>/src/youDependencies.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>/youYour PSR-4 root. Families (Controller, Model, Repository, DataType…) get the house rules regardless of the prefix.
phpcs.xml · phpstan.neon · phpunit.xmlyouPoint at the rules that ship in vendor/ubixsys/ubixcore; add your own overrides below them.
vendor/ubixsys/ubixcoreframeworkNever committed. Upgrade with composer update ubixsys/ubixcore.
bin/ubix · command groups
CommandGroupNotes
code:reviewCodephpcs, 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 · reconcileMigrateApply 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:deployAppRun an app locally, build its image, apply its manifests.
database:*DatabaseSchema reset and bootstrap helpers for local and test databases.
cron:*CronScheduled jobs, loaded only when a cron command is invoked.
ci:aiReviewCiAdvisory 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:requireApprovalCiFails 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>anyExtend Ubix\Console\Command\AbstractCommand under your namespace; bin/ubix discovers it.
Architecture

The framework asks; the host answers.

Nothing in vendor/ knows where your project is or what it is called. The bootstrap tells it, once.

The stack
bin/ubix · public/index.php
your thin entry points → Ubix\Bootstrap
Slim 4 HTTP · PHP-DI · Symfony Console
app/<App>/src/{Dependencies,Middleware,Routes}.php
Your controllers → services
typed payloads · your namespace · house rules by family
Repositories · custom data types
interface-bound · DTO options · framework bases
MySQL / MariaDB · uBixVault
versioned migrations · credentials resolved at boot
How it fits

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.