Adapter-based deploy control plane · apps/spindrift
A deployment is an artifact, a place, and a version of its config.
Pin those three and almost everything people build deploy tooling for stops being a feature. Every slide here reads in one line — open a band if you want the argument.
Four contracts 3 parts to the first 5 adapter seams Skim in 90 seconds
The problem
Nothing on that list is unsolved — clusters, registries, GitOps engines and free certificates all exist. What nobody hands you is the assembly, and the assembly is the work, in full, every single time.
Seven decisions, made by whoever is holding it. Each one is a place to be quietly wrong for a month.
No amount of infrastructure you already own removes any of it. Owning the cluster is what makes the assembly your problem rather than somebody's product.
Contract one · what a deployment is
Artifact, Target, Config — each named by value rather than by reference, so no part of a deployment is allowed to mean “latest”.
deploy 2 · artifact files·sha256:1d7ea… · target bluenose/static · configVersion sha256:4f53cda1…
A real row off the running installation — config_version is the column the digest is stored in. That line is the deployment, not a description of one and not a pointer to one. Reproducing it needs nothing that could have moved since.
Identity is kept separate from placement. What was built never learns where it went — and that one separation is what the rest of this deck is downstream of.
A human writes an App and a Component. Build, Artifact and Deploy are produced, never typed.
Consequences, not features
Rollback, two runtimes, safe concurrency and config that cannot drift are not features here. Each one falls out of having named the three parts by value.
Not a rebuild. Retention depth is 10, which is therefore also rollback depth.
An artifact is image or files. Kubernetes, Cloud Run and static hosting are the same triple with a different where.
The triple is taken under a locking read against the transactional Component@Target.desired row.
Scoped to (Component, Target) and pinned by digest into the deploy — so a running deployment reads what it was deployed with.
Contract two · where the far sides plug in
Every outside thing is swappable without editing at the place it is used — and the proof one is real arrived unannounced: a build route that dials in rather than out landed as one row in a list.
A seam is a place behaviour can be swapped without editing at that place. One implementation would leave a seam hypothetical; every one of these has at least two.
| Seam | What varies across it | Adapters today |
|---|---|---|
| Build | where the artifact is produced | GitHub Actions · Cloud Build · microVM pool · in-cluster Job |
| Deploy | what runtime receives it | Kubernetes · Cloud Run · static hosting |
| Delivery | which operator drives it | Flux HelmRelease · Argo Application |
| Config store | where the value of record lives | Secret Manager · 1Password · HTTP |
| Datastore | where a Target's state lives | cloud project · cluster |
A Target declares which operator drives it, and Spindrift writes that operator's own object. Both flavours end at the same status type and nothing above them branches — so the GitOps engine is a choice a Target makes, never a thing the platform is built out of.
apply · observe · destroy · run · executions · tail · inspect → seven verbs, and “deploy anything, anywhere” is behind them
Three build adapters reach outward — a workflow dispatch, a cloud API, a Job on a cluster this process already holds a token for. The fourth is a pool of microVMs on a machine Spindrift cannot open a connection to at all. Every instinct says that needs a new concept.
It needed a row in a list. The route writes an intent to an outbox row and polls that row for a verdict — exactly the way the other three poll a status endpoint. The endpoint just happens to be its own database.
Bosun keeps a warm pool of ephemeral microVMs, each serving exactly one CI job before it is destroyed. Spindrift does not run it, install it, or know anything about it beyond a label — the host long-polls in over three shared-secret endpoints and claims what it finds.
Two of the three original routes are somebody else's service; this one owes them nothing, and it clears the same SLSA L2 bar — verified against the build hull's own builder identity before core signs the digest. No host serves the class today: the route stays declared and ranked last, so bringing a host back serves it with no configuration change, and a build that falls through to it times out at the build budget rather than hanging.
That is the test a contract passes or fails, and it is not a test you can run on paper. An interface that only admits far sides you are able to dial was never a seam; it was the shape of the three things that happened to exist when it was written. Its deck is a sibling of this one, deployed the same way through the same product: bosun-slides-web.web.app
Contract three · who may reach it
They sit in desired state beside the image and the config, so they are pinned into the deploy like everything else — and a Target that cannot authenticate a reach is excluded before a build is ever dispatched.
Not a policy bolted on afterwards, and not a wiki page about which ingress to copy. A deployment therefore records who could reach it, as a fact, at that version.
Private is the default. Nothing becomes reachable because somebody forgot a field.
A Target declares which reaches it can authenticate, not merely which it can serve. So auth decides where you are allowed to deploy.
An auth: proxy Component gets an ExternalAuth filter on its HTTPRoute, enforced by an authentication proxy the platform owns and Spindrift never installs. Nothing is linked into the application, so an App cannot opt itself out of its own edge.
First run is one screen: an unclaimed installation shows enrolment, a claimed one shows sign-in, and there is no toggle. The token is consumed on use. A passkey is scoped to the origin in the address bar, which is why the hostname is chosen before anything is installed — and why no password exists to phish.
Static hosting refuses auth: proxy outright — there is no non-bypassable origin to put a boundary in front of, so the placement is rejected rather than shipped with a caveat. A guarantee that quietly degrades is not a guarantee.
reach: private · auth: proxy → AUTH_UNSUPPORTED excludes every Target that cannot authenticate at that reach, before a build is ever dispatched
Contract four · where authority stops
Spindrift holds delegated write access to contents and nothing that can create a cluster, a project, a VPC, a tunnel or a signing key. A missing prerequisite becomes infrastructure code on a pull request, not a resource.
A release object in one namespace, an App inside one pre-provisioned project — that is the whole of its write access. When a prerequisite is missing it writes the infrastructure code that would clear it and opens that as a pull request: a human merges, the apply bot applies, and a standing check turns the row green.
Making the artifact half checkable
Because the artifact is named by digest, it is a thing a signature can be about. Four build routes converge on one digest, one key signs it, and two independent policy engines re-check it at admission.
Evidence, not assertion
Uploaded as an archive, built on the hosted route into a files artifact, served at a URL by the thing it describes — and every clause above was proven on a live installation rather than a fixture.
Enrolment. Target connection. Archive to URL. Repository to signed artifact. Admission on a second Target.
Not fixtures — a running installation, with the digests, build numbers and response codes written down each time. An independent confirmer re-observed the last of them from scratch.
That last one is fixed, and this revision is the proof: these words reached you as a second deploy onto the site the first one made.
Three of the four are wrong sentences shown to an operator. No passing suite would ever have caught them.
Three fields on a row. Everything in this deck — rollback, two runtimes, safe concurrency, an auth boundary that cannot be opted out of — is downstream of deciding to name things by value.
The work left before a v1 is known and unglamorous. What is still outstanding is mostly wrong sentences shown to an operator — the failure mode a green suite is worst at catching, and the one the band above is entirely about.
Artifact + Target + Config 5 seams, every one of them real Everything else is bookkeeping