2. The architecture and the plugin model
From the code on monetize main c9b473a5 and the kit (private-monetize-ops main 3265880).
2.1 The processes on the ONE box
| process | started as | listens on | does |
| PID 1, the edge | monetize-admin as korp-start, no arguments |
:80/:443; the operator's door :8475 |
carries /callback/<kind>/<instance> and the estate-facing gRPC services (Fleet, Signup, EstateOrder, Purchase, Products) to the server, refuses every other service (404), carries the rest to the console; supervises the two children (crates/monetize-admin/src/supervise.rs); applies the feed; updates itself |
| monetize-server | monetize-admin server --config /data/monetize/monetize.toml |
127.0.0.1:50061 |
the engine: the guests, the ledger, the orders, the estates, the guardian, the jobs |
| the console | monetize-admin ui --bind 127.0.0.1:8091 --server 127.0.0.1:50061 --root /data |
127.0.0.1:8091 |
the operator's web console (Leptos) and its read-only backend |
The server's background work runs as jera jobs (crates/monetize-server/src/jobs.rs); a failed one is ALERT jobs.failed.
2.2 The crates, by layer
| layer | crates | what they hold |
| the core | monetize |
Plan, Entitlement, the ladder, the transaction, the ledger, pricing, tax; no vendor code |
| the seams | monetize-pay (the Payment trait + the open invoice/sandbox vendors), monetize-cloud (the Cloud trait, the spend cap, the lay and grow types, the DnsWriter trait + the open server/sandbox clouds), monetize-product (the product facts, Pulled, the guardian types) |
what a plugin is seen as |
| the guest SDKs | monetize-product-guest, monetize-pay-guest, monetize-cloud-guest, monetize-dns-guest, monetize-signup-guest |
what a plugin author compiles against (chapter 3) |
| the guest hosts | monetize-product-wasm (products, vendors, signups), monetize-cloud-wasm (clouds, DNS writers) |
load, verify, sandbox, hot-replace |
| the wire | monetize-proto (gRPC), monetize-embed (the signed forms a product verifies: facts, fleet calls, orders), monetize-estate-side (the box pump and the front's shop door) |
what crosses between the box and an estate |
| the service | monetize-server |
the gRPC services, the vendor webhook, the estate orders, the fleet, the guardian runner's world |
| the box | monetize-admin (PID 1, the door, the feed, self-update, alerts, the money log), monetize-front (the feed intake, the attach mint, the ledger backup), monetize-shop-pairing (the installer), monetize-mail (SMTP, TLS always), monetize-cli (the operator's command line) |
the ONE box's own code |
| the site | monetize-site-buy (the buy flow a product site relays; BuySite) |
compiled into Gunnar's and Holger's sites |
| the console | monetize-ui-web (Leptos csr), monetize-ui-server (its BFF) |
chapter 7 |
| tooling | mock-upcloud, mock-scaleway, monetize-fast, fast-edge, lake-product, monetize-build-thing, monetize-box-watcher, estate-svg, guests/*, examples/* |
tests, the mock provider, the fast flow, the watcher outside UpCloud, the drawing, the skeletons |
2.3 The plugin model
A plugin is a signed WebAssembly module the box loads behind the same seam a linked implementation would use; the server cannot tell the two apart (crates/monetize-product-wasm/src/lib.rs:1-16). Five kinds, one mechanism:
| kind | module | block in monetize.toml |
grants it may hold | what it does |
| product guest | gunnar_monetize_guest.wasm, holger_monetize_guest.wasm |
[products.<id>] guest = … |
none | the product's facts: meters, roles, media, the lay plan, the guardian |
| payment vendor | stripe.wasm |
[vendors.<kind>.<instance>] |
http, hmac, state | the ten Payment ops; the webhook |
| cloud | upcloud.wasm |
[clouds.<name>] |
http, state | the nine Cloud verbs; journaled |
| DNS writer | dns_cloudflare.wasm |
[dns_writers.<name>] |
http | the records of a lay |
| signup sink | <product> signup guest |
[signups.<product>] |
grpc, state | the paying customer's account on a non-pulled product |
What stays on the host, always: the credential (added per request from [[http_auth]]), the webhook secret (the hmac door compares), the spend cap, the signing key, the journal file, the allow-list of hosts (http_hosts), fuel, deadline and memory (chapter 3 §3.0, chapter 8).
Where the modules live on the box (crates/monetize-front/src/intake.rs:45-61):
| feed key | file under /data/monetize/ |
plugin.<id>.wasm / .p7s / .toml / .prices |
guests/<id>.wasm, guests/<id>.wasm.p7s; the block and the prices go into monetize.toml |
vendor.<kind>.<instance>.wasm / .p7s / .toml |
vendors/ |
cloud.<name>.* |
clouds/ |
dns_guest.<name>.* |
dns/ |
signup_guest.<product>.* |
signup/ |
plugin_root.<name> (DER, base64) |
trust/<name>.der |
secret.<name> |
secrets/secret.<name> (0600) |
Every write is staged, fsynced and renamed; the signature file is written before the module (intake.rs:23-29, 788-832).
2.4 How the feed installs them
- The operator's kit builds the feed from
solo.toml(admin::solo_feed,private-monetize-ops/xtask/src/admin.rs:337-391;solo::feed_of,solo.rs:1083-1329). Each plugin's bytes must start with\0asmand match thesha256pin insolo.toml([solo-not-wasm],[solo-product-pin],[solo-stripe-pin],[solo-cloud-pin],[solo-dns-pin]); the plugin root must match[plugin_root]. The signed release list is verified first (release::verify_box). Only key names and byte sizes are ever printed. cargo xtask feed --where upcloud --box <box> --applysends the whole object withPUT /feedon the door:8475(mutual TLS; CNinstalleronly, anyone else 403[not-the-operator];crates/monetize-admin/src/door.rs:1-3, 60-62, 71-124).- The door lays the console's own sites and accounts over the feed (the app wins by name), judges it with monetize's own loader in a scratch directory (
solo/judging), and only then writes it 0600 to/data/solo/door-feed.json. A feed the server would not start on (a priced product with no vendor, a guest with no trust roots, …) is refused whole with 422 and the box keeps serving what it had. - PID 1 looks every 2 s; when the feed moved it runs
intake::apply_pull_linked(crates/monetize-front/src/intake.rs:845-1067), in this order: trust roots, secrets, vendor guests, product guests, config and control principals, cloud guests, signup guests, DNS guests, the operator'smonetize.tomlfragment, then the renderedmonetize/monetize.tomlwritten atomically only if it changed. It recordssolo/door-feed.taken(the sha256, or<sha> NOT taken: <why>); the kit pollsGET /feed/applied(200 applied, 202 applying, 422 refused). - The server's guest watcher (
crates/monetize-server/src/guests.rs:95, every 2 s) sees the new module, signature, root or block and runs the full load order (chapter 3 §3.0) with no lock held; a changed guest is swapped in with one pointer swap, a new id is published in one of 64 live slots, a refused one keeps the serving guest. Nothing restarts for a guest. A changedplugin.<id>.pricesrestarts monetize-server; a changed console setting restarts the UI (crates/monetize-admin/src/solo.rs:366-376, 452-457).
PUT /feed/next stages a feed for the NEXT app pin (/data/solo/door-feed.next.json): that app takes it at its first start, and it becomes the door feed only once that app is confirmed, so a rollback leaves the old feed untouched (door.rs:126-162).
2.5 Held plugins
"Held" has exactly two meanings in the code:
- A product with no home site yet. A product block with no
instance, noendpoint, no gRPC hosts and no twin is HELD by the intake: not served, not refused (crates/monetize-front/src/intake.rs:216-231). The kit leavesinstanceout for a product that has no site on this box (solo.rs:1163-1169). The first operator site added for that product becomes its home (crates/monetize-admin/src/app_config.rs:985-990); the overlay then writesinstance = <label>intoplugin.<id>.toml(:393-403) and the block is served. A non-operator first site is refused[site-product-homeless]. - A plugin-update row for a product guest no block runs yet answers
held/[plugin-held] no site of the product <name> on this box yet: its guest is taken from the feed at its first site(crates/monetize-server/src/plugins.rs:204-215). Any other kind with no block is refused[plugin-no-block]. The console counts held rows as current (crates/monetize-ui-web/src/plugins.rs:97-109).
2.6 Plugin update
The console's "Update plugins", monetize box plugins [--apply], or the door's POST /box/plugins queue one jera job (crates/monetize-server/src/plugins.rs:1-22, 269-412):
- Read
release/plugins/box/<box>/release-manifest.toml+.p7sfrom the release bucket (the sharedrelease/plugins/release-manifest.tomlfor a box with noplugins_box), through a presigned URL with the fed read pair (secret.release_objsto, elsesecret.media_objsto). The list is at most 256 KiB. - Verify the list's signature against the box's
guest_trust_roots. - For every row with role
plugin,vendor,cloud,dnsorsignup: fetchrelease/plugins/<blake3>/<name>(at most 64 MiB) and its.p7s(64 KiB); check length, sha256 and blake3 against the list; identify the guest's kind and name from its manifest; verify the.p7sagainst each block's roots. All of this before anything is written. - The outcome per row:
unchanged(identical bytes),replaced(written beside, renamed over, the.p7sfirst;would-replaceon a look without--apply),held(§2.5), orrefused(ALERT plugin.refused; the other rows go on). - The guest watcher takes the new file in within 2 s. Nothing restarts.
There is no version number on a plugin: identity is the blake3, and a row is replaced whenever its bytes differ from what is installed. The guard is the signed list. (A compatibility refusal exists apart from that: a vendor module with the old twenty-op table is [vendor-old-table].)
The kit publishes the list and the modules with cargo xtask release --sign-with <dir> --apply --publish (the plugin-signing key plugin-signing.pk8 + plugin-signing.der from --sign-with or $DWARVES_PLUGIN_SIGNING_DIR; a detached CMS over release-manifest.toml), uploading each module and its .p7s first and the list LAST (private-monetize-ops/xtask/src/release.rs:346-410, 518-540).
2.7 The ONE box's own updates
| path | when | how long | what |
| self-release → box update | the common case: a new monetize release on the same BASE | about 30 s on the box (measured on test2: asked 23:38:06Z, served 23:38:36Z) | cargo xtask self-release --publish builds and signs the box's next app, PUTs the archive at release/monetize-admin-live/<pin>.live-app.znippy and the record LAST at release/monetize-admin/boxes/<host>/next.live.toml; monetize box update --apply (or the console's "Update this box") stages it into the other app slot under the root enrolled at install, re-execs PID 1, and confirms after 60 s of serving whole (monetize answers on 50061 and serving.json lists every product); not served by 600 s = ALERT self-update.rollback and korp rolls back (crates/monetize-admin/src/self_update.rs:1-76) |
| upgrade (the keep boot) | a BASE change (another korp base system), live update off, an unsigned app | about 17 min | cargo xtask upgrade: a keep medium (reinstall=keep, install_then=poweroff) is booted from the cdrom, rewrites the ESP and the rootfs, keeps /data, powers off; the box is proven again (its build, the recorded monetize_pubkey, the edge on :443, the feed it held) (private-monetize-ops/xtask/src/upgrade.rs:1-51) |
decide picks the path in this order: a trial still open → wait; the same app → nothing; the other slot holds this app unconfirmed → rolled back, never offered again; live update off, or another BASE, or unsigned → Plan::Base (use upgrade); else live (self_update.rs:220-251). /data is never touched by either. The way back is box update --back, only to a release that was confirmed on this box.
Sold estates and sites are updated by the box through UpdateEstate (chapter 5 §5.5).