Control plane is an application
Configuration and Trident define the control plane; no product-specific runtime is required.
- Config services + policy
- Trident modules control workflows
- Shared runtime MTK mechanisms
- Control plane operating application
2027 · MerTek technology
Platform product / System overview
The control plane, built as an MTK application
MerTekStak is the control plane that provisions hosted runtimes and decides which generation of an application runs. It is itself an MTK application: configuration plus Trident on the shared runtime, with its own store. Every write names the application, tenant and generation it expects, and every release keeps a digest chain from source to shipped bytes.
Worked example
Northvale Health books appointments for 40 clinics through a hosted scheduling app. At 07:55 two operations analysts publish live edits built on generation 184, seconds apart. The first changes Saturday slot lengths, and at 08:02 Saturday bookings start failing.
Every write names what it expects to change. The ownership guard compares the application, the tenant and the expected generation with what is stored, before anything is written. An edit prepared against generation 184 cannot land on top of 185.
# s/guard.tri: every control-plane write passes this one gate.
outline writeTarget {
text application = ""
text tenant = ""
u64 expectedGeneration = 0
}
private void guard(writeTarget want, text application, text tenant, u64 newest) {
if (want.application != application) { fail "wrong application" }
if (want.tenant != tenant) { fail "wrong tenant" }
if (want.expectedGeneration != newest) { fail "stale generation" }
}
text main(text[] argv) {
writeTarget late = { application = "northvale.booking", tenant = "northvale", expectedGeneration = 184 }
text outcome = "written"
task {
guard(late, "northvale.booking", "northvale", 185)
} else (refused) {
text why = refused.message
outcome = "refused: " + why
}
return outcome # refused: stale generation
}Keep every generation as a row. A live edit becomes a generation only after its candidate compiles in a check-only pass. Each row keeps the verified bytes that ran, and a unique claim key lets exactly one edit win each generation number.
# _db/troves/appGeneration.tri: appended, never rewritten.
trove appGeneration {
text application = ""
text tenant = ""
u64 number = 0
text claimKey = ""
sha256 source = "0000000000000000000000000000000000000000000000000000000000000000"
buffer wire = 0x
u64 rolledBackTo = 0
check backward = rolledBackTo < number
unique index byClaim = claimKey
}Claim, then publish. Publication appends the next row with the claim key application#number. A typed write is conditional. Both 07:55 edits passed the guard and raced for generation 185. The unique index let the first one win and refused the second as superseded, and nothing half-applied.
# s/publish.tri: append generation N+1 while this edit still holds the base.
private text publish(writeTarget want, sha256 source, buffer wire) {
u64 next = want.expectedGeneration + 1
text key = want.application + "#" + text(next)
task {
appGeneration.insert({ application = want.application, tenant = want.tenant, number = next, claimKey = key, source = source, wire = wire, rolledBackTo = 0 })
} else (lost) {
return "superseded"
}
return key
}Roll back by replaying stored bytes. A rollback is one more row: generation 186 carries 184's verified bytes and names 184 as its source. No compiler, source checkout or build machine runs during a rollback.
# s/rollback.tri: the newest generation becomes a copy of an earlier one.
# It runs only after guard(want, ...) has passed.
private text rollBack(writeTarget want, appGeneration prior) {
u64 next = want.expectedGeneration + 1
text key = prior.application + "#" + text(next)
appGeneration.insert({ application = prior.application, tenant = prior.tenant, number = next, claimKey = key, source = prior.source, wire = prior.wire, rolledBackTo = prior.number })
return "generation " + text(next) + " replays " + text(prior.number)
}Every release carries its digest chain. The release record links the source digest, each gate verdict with the digest of its evidence, the artifact digest, and the record before it. An auditor recomputes each digest from the stored bytes.
# The release record of generation 185.
text application = "northvale.booking"
u64 generation = 185
sha256 source = "9c1f0e7a4b2d8c3e5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f7089a1b"
chest[] gates = [
{ text gate = "booking-suite-test", text verdict = "pass", sha256 evidence = "2b8e61c0f4a95d37e1b20c8f6a4d93e7c5b1a0f2e8d47c6b39a1f05e2d8c7b46" },
{ text gate = "saturday-slots-tape", text verdict = "pass", sha256 evidence = "71d04e9a3c6b2f85e0a1d7c49b3e62f8a05d1c7e94b2a6f30c8e5d1b7a4f9e20" }
]
sha256 artifact = "c40e8b17f2a6d95e3b01c7a4f8e2d6b90a5c3e1f7d48b2a6e9c05f13d7b8a26e"
sha256 previous = "e5a91f3c07d2b84e6a1c9f05d3b7e28a4c6f10d9e2b75a3c8f41e6d0b9a27c53"Saturday bookings recovered at 08:04, two minutes after the first failure, from bytes the control plane already held. The racing edit was refused as superseded instead of landing on an unknown base. The record showed generations 184, 185 and the rollback 186, each with its source digest and gate results.
System diagram
A rollout is a stateful record, not a private shell script.
Inside the system
Follow the data, decisions, and access boundaries. Each diagram explains a concept used by applications.
Configuration and Trident define the control plane; no product-specific runtime is required.
Every write proves the application, tenant, and generation it intends to change.
An edit becomes a generation only after it validates and wins its claim. A rollback appends a generation that replays earlier bytes.
A release record links the source digest, each gate verdict, the artifact digest and the record before it.
A module crosses the product boundary only through the declared shared surface.
Key parts
These are the main boundaries, inputs, outputs, and failure rules. The examples show a specific use of each part.
MerTekStak is configuration plus Trident on the shared runtime. It has no compiled shortcut the apps it manages lack, and its records live in its own store. Each customer runtime keeps a store of its own, and no write crosses between them directly.
MerTek.io signs a customer in and sends an idempotent provisioning request. MerTekStak enacts it and returns the durable outcome; a request row alone proves nothing was provisioned.
Every write states the application, the tenant and the generation it read. A mismatch on any of the three refuses with a named reason and writes nothing. The guard runs before every control-plane write.
A script aimed at the staging tenant cannot change production by accident, because the tenant it names does not match.
A live edit becomes a generation only after it validates and wins its claim. Each generation keeps the verified bytes that ran, so a rollback replays them. A release record links source, gate verdicts, artifact and the previous record by digest.
An auditor can recompute every digest in a record and find the exact gate results a generation shipped with.
Code
Each example is short and complete enough to read on its own. Hover a dotted word for its definition.
The control plane uses the same program roots as any app. The operator console is a client build and never compiles the provisioning code, and both builds read the shared contracts from a/.
mertekstak/
a/ shared contracts both builds read
provision.tri the request shape MerTek.io sends
generation.tri the generation states every view shows
s/ the control-plane server
main.tri
guard.tri the ownership guard
publish.tri validate, claim and publish
rollback.tri replay a stored generation
c/ the operator console
main.tri
history.tri generations, actors and digests
_db/troves/ appGeneration and provisioning records
MerTek.io sends the same request again after a timeout with the same key, so a retry is one request, never a second runtime. The request travels as a shared outline both builds can read.
# a/provision.tri: the same key twice is one request.
outline provisionRequest {
text requestKey = ""
text application = ""
text tenant = ""
text role = "server"
}
The caller reads the fault's message in the task's else arm. A wrong tenant and a stale base are different refusals, and an operator sees which one stopped the write.
# expected generation 184, newest 185, same tenant
# refused: stale generation
# expected generation 185, tenant "northvale-staging"
# refused: wrong tenant
# expected generation 185, tenant "northvale"
# written
task {
guard(want, "northvale.booking", "northvale", 185)
} else (refused) {
text why = refused.message
outcome = "refused: " + why
}
Generation 186 carries the same source digest as 184 and names it in rolledBackTo. Generation 185 stays in the history, so the record shows what ran between 07:55 and 08:04.
# The generation rows behind the 08:04 rollback.
chest[] generations = [
{ u64 number = 184, text claimKey = "northvale.booking#184", u64 rolledBackTo = 0,
sha256 source = "4e17b09c2d8a63f51e0c9b7a2d4f86e3c1a5b9d07e2f48a6c3b1d9e05f7a2c84" },
{ u64 number = 185, text claimKey = "northvale.booking#185", u64 rolledBackTo = 0,
sha256 source = "9c1f0e7a4b2d8c3e5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f7089a1b" },
{ u64 number = 186, text claimKey = "northvale.booking#186", u64 rolledBackTo = 184,
sha256 source = "4e17b09c2d8a63f51e0c9b7a2d4f86e3c1a5b9d07e2f48a6c3b1d9e05f7a2c84" }
]
Provisioning records live in MerTekStak's own store. A customer runtime receives neither that trove nor that store, so no application can enumerate or create another.
# who store
# MerTekStak server runtime its own: desired state, provisioning, fleet, metering
# MerTek.io its own: identity and product records
# a customer server runtime that customer's application store
# a client runtime its client cache, filled by the server
#
# An enacted-app registry is an ordinary trove of the control plane.
# Only the control-plane program names it.
Uses
Review questions
Who may publish a generation for each tenant
How long generation bytes are kept for rollback
Which gate results a generation must carry before it runs