Push to Deploy
Overview
Every Kinotic project is backed by a git repository, and pushing to that repository's default branch deploys the project. There is no CI to configure and no pipeline file to maintain — the platform is the pipeline. Each qualifying push is verified, built, and deployed as a tracked job you can watch live.
Only pushes to the default branch deploy. Pushes to other branches, branch deletions, and tag operations are ignored.
What a deployment does
A deployment runs as a job with six steps, visible with live progress on the project's Deployment page (and, like any job, on the Jobs page):
- Resolve deployment target — the first deployment picks a node with capacity and creates the project's checkout directory on it; later deployments reuse the same node and directory.
- Sync project source — a short-lived, sandboxed build VM brings the checkout to the
pushed commit (an incremental fetch, not a fresh clone — installs stay warm), installs
dependencies, finds the artifacts the commit contains, and compiles the
project. This step is the build gate: a commit that does not build, or whose packages
are not named validly, never reaches your running services. Your entity definitions are
synchronized and published here, so an entity added in this commit has its backing
storage created and is ready for data operations; entities already published keep
serving. Pending migrations in
./migrationsare applied in the same step. The artifacts it found are reported to the platform, with the hash of the dependencies it installed, and only after everything succeeded does it signal the runtime to reload. The step expands on the job run page into the build VM's log, live while it runs; the VM is destroyed once its run ends, whatever the outcome, and the log stays readable from the run's history. - Resolve artifacts — binds the artifacts the build VM reported for this commit into the deployment run. The step's row on the job run page lists them: each microservice with the module it starts from, and each UI.
- Ensure runtime workloads — one long-lived VM per microservice the commit contains, each running that microservice from the checkout (mounted read-only) with its own machine identity. The step records what each microservice's deployment should be, running the commit, and waits for the deployment's worker to answer. The first deployment of a microservice starts its VM. Later deployments leave a running VM alone: its supervisor sees the reload signal and restarts the service onto the new commit. A VM whose entry module the commit moved is stopped and replaced by a fresh one, and so is one whose run has ended — never started again with the state that may have failed it. A microservice the commit no longer contains keeps its VM running but its deployment is marked orphaned, and a commit that brings it back adopts the VM; the deployment is only ever removed on request. A microservice that cannot be left running is recorded failed with the reason, the others still deploy, and the step fails naming it, or naming the ones whose worker gave no answer within ten minutes.
- Publish UIs — uploads every UI the build VM built to its site's directory in the platform's storage through a short-lived publish VM that holds nothing but one upload URL per site, valid for the run, then records what each UI's deployment should be, serving the commit, and waits for the deployment's worker to look. A UI's first publish mints its site's hostname; nothing else is created for a site, so it serves as soon as its files are up, and later publishes switch it to the new commit. A UI the commit no longer contains keeps serving but its deployment is marked orphaned, and a commit that brings it back adopts the site. The step expands on the job run page into the publish VM's log.
- Generate SBOM — keeps the project's SBOM current. When the project has no SBOM
of the dependencies the build VM installed, a short-lived SBOM VM reads the dependency tree of
the checkout's
bun.lock, mounted read-only, and records it with the platform; otherwise no VM runs and the SBOM stays. The step expands on the job run page into the SBOM VM's log, or says the dependencies are unchanged. A checkout without abun.lockfails the step.
The runtime supervisor restarts your services as whole processes, with escalating backoff if they crash immediately after a reload.
Artifacts
A project repository is a Bun workspace, and every deployment finds the artifacts it contains by where each package sits in the tree. Two kinds are deployed today:
- A microservice is a directory directly under
packages/microservicesholding apackage.json. Its entry module is themainof thatpackage.json, orsrc/main.tswhen it declares none. - A UI is a directory directly under
packages/uiwhosepackage.jsondeclares abuildscript. A package underpackages/uiwithout one is a library, not a UI, and is left alone.
An artifact's identity is the unscoped part of the name in its package.json:
@acme/orders is the microservice orders, @acme/admin the UI admin. The directory
name never matters, so a package can be moved or its directory renamed without changing
what the platform knows it as. The unscoped name must be lowercase letters, digits, and
interior dashes, a UI's must not contain --, and two artifacts of the same kind cannot
share one; any of these problems fails the deployment in the build VM with a message in its
log naming the package. Nested
package.json files deeper in a package are never artifacts of their own.
The artifacts are found inside the build VM, over the checked-out commit, and recorded on the project's deployment together with the commit they were found in and the hash of the dependencies installed for them.
Machine identities
Every VM connects to Kinotic as a machine identity the deployment provisions for the project,
one for the build VM (machineType PROJECT_SYNC), which the SBOM VM that runs after it shares,
and one per microservice (APP_RUNTIME),
recorded with the deployment. They are organization-scope: synchronizing entity definitions and publishing services into your
application's zone are things your organization does on its own behalf, so an identity scoped
to a single application would not be enough to do either. The project's Deployment page lists
them, so you can see which credentials exist for a project and what each one is used for.
Kinotic stores only a hash of a machine's secret, so a secret can never be handed out a second time. Every deployment therefore issues the build VM a new secret, and the one the previous build used stops working at that moment; an SBOM VM, which runs once the build VM has exited, is issued a newer one again. A microservice's secret is issued when its VM is created and stays valid for that VM's life — a later deployment reloads the service inside the running VM rather than replacing it.
A project's machines can be disabled or removed like any other machine, which cuts the corresponding workload off at its next connection. Removing one is not permanent damage: the next deployment provisions a replacement.
Microservices
The project's Deployment page lists each microservice the deployment has ensured: the phase it
reports, the commit it serves, the module it runs, and its workload. From there you can open
the microservice's log, restart it, or remove the deployment; the listing and the restart are
MCP tools too, so an assistant connected to the platform sees each microservice's phase, its
message and a restart it is waiting for, while removal, which only the next deployment undoes,
stays with the console. Each deployment carries what it
should be in state.desired — the commit the project's last deployment asked it to run, or
ORPHANED when that commit dropped it — and what it is in state.observed; failureMessage
says why the two differ when they do. A deployment's worker keeps the two equal: a VM whose
run ends is replaced; one that failed waits first, thirty seconds doubled by each failure since
the last deployment up to ten minutes, with restartAt saying when, so a crashing service costs a
VM every ten minutes at most, and a new deployment or a restart starts it again at once; a VM on
a node the platform cannot reach is left alone,
since it may well be running, and the deployment carries NODE_UNREACHABLE in
state.conditions until the node answers or is deregistered. Restart stops the running VM,
and the worker replaces it once the run has ended; a deployment without a running VM is asked
to deploy again. Removing stops the VM, deletes its machine identity and the record; a
microservice the current commit still contains comes back with the next deployment, so removal
is mainly how an orphaned microservice, one a commit dropped, is finally retired.
| Reported phase | Meaning |
|---|---|
DEPLOYED | The VM is up and running the microservice as of the commit in state.observed |
FAILED | The last deployment could not leave the microservice running; failureMessage says why, and the next push or a restart tries again |
ORPHANED | The last deployed commit no longer contains the microservice; the VM keeps running until removed |
UIs
Each UI is served from its own site under the organization's subdomain of the platform's sites
domain, at the hostname <applicationId>--<uiName>.<organizationId>.kinotic.io its first publish mints. A UI name belongs to
one project of the application, so a second project publishing a UI of the same name fails
its deployment until the first project's UI deployment is removed. The site serves the
UI as of the commit recorded on its deployment, and a publish switches it atomically: the
new commit's assets are uploaded first, under a path named by the commit and cached for a
year, and the site's index is replaced last.
Each deployment carries what it should be in state.desired — the commit the project's last
deployment published, with the phase READY, or ORPHANED when that commit dropped the UI —
and what it is in state.observed; while the two differ, observation says what the site
answered when last checked. The deployment's worker checks the site every thirty seconds until
it serves the published commit.
| Reported phase | Meaning |
|---|---|
PROVISIONING | The site does not yet serve the published commit; the commit is the one it still serves |
READY | The site serves the UI as of the published commit |
ORPHANED | The last deployed commit no longer contains the UI; the site keeps serving until removed |
Removing a UI deployment asks the worker to take its site down, delete the UI's published files and delete the record; a UI the current commit still contains comes back with the next deployment, at a site minted anew. Deleting the project asks for the removal of its deployment, whose worker asks for the removal of every one of its UI and microservice deployments the same way, then stops the deployment's workloads and removes its machine identities.
The project's deployment page lists each UI with its site, the phase it reports and the commit the site serves, and offers removal; the project's and the application's overview pages list the published UIs with their sites as well. The listing is an MCP tool too, so an assistant connected to the platform sees each site's phase and what it last answered; removal, which only the next deployment undoes, stays with the console.
SBOM
Every project has a software bill of materials: every package its bun.lock installs, the
dependencies of its microservices and UIs and the tools that build them, each with its version
and package URL, whether one of the project's own package.json files declares it or it arrives
transitively, and whether it is used at runtime, only in development, or optionally. The SBOM
also records which package depends on which, resolved the way bun loads them, so any package can
be traced back to the declared dependencies that bring it in. The platform reads it from
bun.lock itself, so a project needs nothing of its own to have one. The project's own workspace
packages are the project rather than its dependencies, and are not listed.
The SBOM is read again only when the project's dependencies change. The build VM hashes
bun.lock and reports the hash with the commit's artifacts; a report whose hash differs from the
previous one drops the project's SBOM, and the last step of the deployment reads it again. A push
that changes code but no dependency keeps the SBOM.
The project's SBOM page shows the SBOM of the last synced commit, lists its packages,
searchable by name and filtered by how the project uses them, and downloads it as a CycloneDX
1.6 JSON document, the dependency graph included. ProjectService.findDependencies serves the
same tree to any client of your organization, MCP hosts included, whenever it lists the current
dependencies; sbomGenerated on ProjectService.findDeployment says whether it does.
A failed SBOM step fails the deployment like any other step, although it runs after the
microservices and UIs are deployed: they keep running the new commit, the deployment is recorded
FAILED with the reason, and the project has no SBOM until a later deployment reads it, since
the dependencies it would list changed. The step reads bun.lock lockfile versions 1 and 2, and fails
on any other lockfile version and on a dependency tree too large for one message to the platform,
around 30,000 packages.
Deployment status
The project's deployment record keeps what the deployment should be beside what it is, under
state. state.desired is the commit the last qualifying push asked for, always with the phase
RUNNING; state.observed is the phase the deployment is in and the commit it serves:
| Observed phase | Meaning |
|---|---|
DEPLOYING | A deployment job is running for the latest qualifying push; the commit is the one still live |
RUNNING | The commit built successfully and every microservice's VM is serving it |
FAILED | The last deployment failed; failureMessage says why, and the commit is the one still live |
The deployment is in its desired state, state.reconciled, exactly when the two agree, and every
change to either is entered in the kinotic_watch_event ledger with what caused it. A change and
its entry land on the record in one write, so every change is entered, at the latest by the
reconcile master's next look at the record or before the record is deleted, and a record's entries
are stamped in the order its writes landed. The ledger is read back newest first: ProjectService.findDeploymentHistory lists what happened to
the project's deployment and to the deployment jobs, build VMs, microservice deployments and UI
deployments it made, which the portal's deployment page shows under History, and
ProjectService.findAllDeploymentHistory lists the same entries for every project in the
organization in one listing, limited to the kinds of entries asked for; MicroserviceDeploymentService.findHistory and UiDeploymentService.findHistory
list what happened to one deployment and to the VMs or uploads it ran, and both are MCP tools, so an
assistant can answer how a deployment got to where it is. The project's
Deployment page shows the observed phase alongside the served commit, the commit being deployed
while a job runs, and the most recent deployment job's steps, live while a deployment runs — so
it always answers exactly what the last push did, step by step.
Failure behavior
A failed build fails the deployment and leaves the previously deployed commit running — the runtime VMs are only ever signaled after a successful sync. The build VM of a failed sync is kept (not discarded) so its logs can be inspected from the workload logs view; it is cleaned up on the next successful deployment of the project.
Concurrent pushes
A push writes the project's intent, state.desired, and the reconcile master, one elected server
node, runs the deployment for it. Deployments are serialized per project with latest-wins:
pushes arriving while a deployment is running collapse to the newest commit, because the record
holds only the latest intent and the master calls the deployment worker once more when the run
ends. Intermediate
commits are skipped rather than queued — the checkout converges to the newest push, and
each skipped commit is still reachable in git history. Redeliveries of the same push are
harmless: syncing a commit twice converges to the same checkout.