Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

docs/v2/02-architecture-and-plugins.md

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

  1. 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 \0asm and match the sha256 pin in solo.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.
  2. cargo xtask feed --where upcloud --box <box> --apply sends the whole object with PUT /feed on the door :8475 (mutual TLS; CN installer only, anyone else 403 [not-the-operator]; crates/monetize-admin/src/door.rs:1-3, 60-62, 71-124).
  3. 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.
  4. 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's monetize.toml fragment, then the rendered monetize/monetize.toml written atomically only if it changed. It records solo/door-feed.taken (the sha256, or <sha> NOT taken: <why>); the kit polls GET /feed/applied (200 applied, 202 applying, 422 refused).
  5. 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 changed plugin.<id>.prices restarts 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:

  1. A product with no home site yet. A product block with no instance, no endpoint, 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 leaves instance out 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 writes instance = <label> into plugin.<id>.toml (:393-403) and the block is served. A non-operator first site is refused [site-product-homeless].
  2. 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):

  1. Read release/plugins/box/<box>/release-manifest.toml + .p7s from the release bucket (the shared release/plugins/release-manifest.toml for a box with no plugins_box), through a presigned URL with the fed read pair (secret.release_objsto, else secret.media_objsto). The list is at most 256 KiB.
  2. Verify the list's signature against the box's guest_trust_roots.
  3. For every row with role plugin, vendor, cloud, dns or signup: fetch release/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 .p7s against each block's roots. All of this before anything is written.
  4. The outcome per row: unchanged (identical bytes), replaced (written beside, renamed over, the .p7s first; would-replace on a look without --apply), held (§2.5), or refused (ALERT plugin.refused; the other rows go on).
  5. 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).