Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

From your laptop to a customer page

You never publish anything. You submit a source zip; a HomeRunner administrator reads it, builds it on their machine, and publishes the result to the plugin CDN. Everything a customer loads was produced by that person, from your source, after a human decided to allow it.

That one fact shapes the whole pipeline and explains its two most surprising properties: your build output is audited after you have been told your submission was accepted, and nothing anywhere sends you a notification. This page is the map; every rule it mentions is stated normatively on another page, and linked.

The pipeline at a glance

ACTOR         STEP                                        ARTEFACT / STATE
──────────────────────────────────────────────────────────────────────────────────
you           npm run build                               dist/ + a stamped manifest.json
you           zip the project folder                      source zip
                │
your browser    ├─ source-zip audit                       a failure sends nothing at all
your browser    ├─ presigned PUT ────▶ PRIVATE BUCKET     submissions/{slug}/….zip
                │                                          (bytes never transit a server)
dashboard       └─ metadata leg ─────▶ CENTRAL            submission = pending
                │
admin           ├─ downloads the zip, reads your source
                │    changes_requested / rejected ────────▶ back to you, review notes only
                ▼    approved                              visibility fixed here
admin           ├─ runs YOUR build, zips the result       built zip
admin's browser ├─ built-zip audit                        fails = no publish, silently
admin's browser ├─ presigned PUTs ───▶ PLUGIN CDN         {env}/{slug}/{version}/…
                │    the root manifest.json uploads LAST   (the version prefix is immutable)
dashboard       ├─ fetches that manifest back             proves the CDN really serves it
dashboard       └─ publish call ─────▶ CENTRAL            one txn: manifest_url,
                                                           current_version, manifest_cache,
                                                           status = approved
                │
dashboard       └─ warm the renderer, purge every feed    live in seconds, not cache TTLs
                │
customer        ├─ feed ▸ Plugins ▸ Marketplace ▸ Install
customer        └─ Create widget from your plugin         the widget row stores the keyword
                                                           plugin:{slug}[:{widget}]
                │
renderer        resolves that keyword against the         your component renders on a
                cached ROOT manifest                       customer's page

Who acts, and where you watch it

#StepWho actsWhere you see it
1Build and zipYouYour terminal
2UploadYouThe uploader in Dashboard ▸ Plugins ▸ My plugins
3Record the submissionCentralYour plugin row flips to a pending badge
4ReviewAn administratorBadge + review notes on the same row
5Build your source, publish the built zipThe same administratorNothing until it lands
6Verify and repointDashboard, then centralThe row goes live; a “Published manifest” link appears
7Warm and purgeDashboardNothing — it is invisible and best-effort
8Install and configureYour customerNothing you can see

Steps 3 through 7 are one machine sequence with exactly one human decision in it — step 4. After step 2 you cannot influence anything except by submitting again.

The eight steps, in words

1. You build

npm run build produces dist/ and stamps runtime.widgetCore into your tracked manifest.json. That stamp is the platform’s evergreen-runtime gate and the reason a build older than widget-core 0.10.0 cannot be published; it is written by the build and must never be hand-edited. See Install and versions.

2. You zip and upload

(Both shapes.) You zip the project without dist/ and node_modules/ — the reviewer rebuilds those. The uploader audits the archive in your browser first: if it fails, nothing is transmitted at all. On success your browser PUTs the bytes straight into a private bucket using a short-lived presigned URL, so the zip never passes through a dashboard server, and a second call registers the metadata with central.

Two consequences. First, media/ and (for suites) widgets/*.manifest.json must be committed — nothing but your build runs later, and nothing generates them. Second, the browser audit is convenience, not authority: central re-validates everything server-side and adds rules the browser cannot check. Both rule sets, with every limit and every verbatim message, live in Packaging and publishing rules.

3. Central records the submission

The submission lands on your plugin row: status pending, your version, your parsed manifest, your notes. A new slug creates the row and you own it; a slug owned by someone else, or a legacy admin-registered row with no owner, is refused — see the API codes in Packaging and publishing rules.

4. A human reviews it

An administrator downloads your zip, verifies its checksum, and reads your source; for an update they also see a diff of the live manifest against yours. Three outcomes:

OutcomeNotesWhat happens
approvedoptionalYour submission waits for a publish. Public-or-private visibility is fixed here, in the approve dialog.
changes_requestedrequiredThe notes appear on your row. Fix and re-submit.
rejectedrequiredThe notes appear on your row, and your uploaded zip is deleted from the bucket.

Nothing is live yet. Approval is a decision, not a release.

5. The same administrator builds your source

They run your build however they like, zip the built project folder, and drop it into the publish card. A second, different audit now inspects what your build actually produced: declared assets and their hashed twins, the registration marker in every client bundle, sub-manifests, icons, screenshots and READMEs. This is the first moment anyone checks your build output, and it happens after you were told your submission was fine. A failure here does not reopen the review — it costs you a resubmission with a bumped version. Both audits are listed in full, with their exact messages, in Packaging and publishing rules.

6. The version prefix, verification, and the repoint

Files upload one by one into the immutable CDN key prefix {env}/{slug}/{version}/, root manifest.json last. That ordering makes a half-finished publish safe to retry: until the root manifest exists, nothing is live and the sentinel that would refuse a re-publish is absent.

The dashboard then fetches the published manifest back from its public URL and refuses to continue unless it serves and matches the submission — that is what “published” means here, not “we assume the bucket worked”. Only then does central repoint manifest_url, current_version and its cached copy of your root manifest, set the plugin to approved and clear the submission, in one transaction. Publishing therefore also un-suspends a suspended plugin.

Where those URLs resolve to is Keywords, assets and URLs; what a version buys you, and how to undo one, is Publishing, versions and rollback.

7. Warm and purge

Composed customer pages are cached aggressively, so publishing does not wait for TTLs to expire: the dashboard asks the renderer to load your new SSR bundle and purges the page caches of every feed that has your plugin installed. Both are best-effort and neither can fail the publish. Typical propagation is seconds; the documented worst case is the composed-page cache TTL for feeds the fan-out did not reach in its budget. The numbers are in Publishing, versions and rollback.

8. A customer installs it, per feed

Nothing is global. A customer opens a feed ▸ Plugins ▸ Marketplace, installs your plugin, activates it, and only then can create a widget from it. The widget row stores your keyword; the renderer resolves it against the root manifest central cached at publish and loads the bundles it names. Install, per-widget toggles, uninstall and the kill switches that can stop your plugin serving are in Install, customers and kill switches; the keyword and manifest side is Manifest: root fields.

The five states you can be in

Your row in Dashboard ▸ Plugins ▸ My plugins renders one of exactly five timelines.

StateStepperWho is blockedWhat to do
pendingSubmitted ✓ · In review · Approved · PublishedAn administratorWait
changes_requestedSubmitted ✓ · Changes requested ⚠ · Approved · PublishedYouRead the notes, fix, re-submit
rejectedSubmitted ✓ · Rejected ✕ · Approved · PublishedYouRead the notes, fix, re-submit
approvedSubmitted ✓ · Reviewed ✓ · Approved ✓ · Publish pendingAn administratorWait
published (nothing in flight)Submitted · Reviewed · Approved · Published ✓NobodyShip the next version

Review notes are shown inline for the two failure states, followed by “— fix and re-submit the zip above.” A plugin that is live and has an update in review shows both at once: the liveness badge for what is serving, and a timeline sitting at In review.

Nobody will tell you

Not supported yet. There are no notifications of any kind. No email, no in-app alert, no webhook fires when your plugin is approved, rejected, published or suspended. Nothing in the pipeline sends one. The only channel is opening Dashboard ▸ Plugins ▸ My plugins and reading the badge, the timeline and the review notes yourself. Poll it.

Not supported yet. The built-zip audit’s warnings are visible only to the reviewer. A config schema that does not extend the base, an oversized bundle, a filename that cannot be published — none of them block the publish, and all of them are recorded in a browser you will never look at. Unless the reviewer copies them into review notes, you will never learn that your plugin shipped with an empty settings form. Run those checks yourself: Preflight and submit.

A third asymmetry to plan around: the one-shape rule is not enforced before upload. Flipping a live plugin between the single-widget and suite shapes passes the source audit and passes review, and only dies at publish. See The two manifest shapes for the decision and Packaging and publishing rules for the gate.

Re-submitting replaces whatever is in flight

(Both shapes.) Uploading a new zip for the same slug overwrites the in-flight submission from any state — pending, changes_requested, rejected, even approved-but-unpublished. The status returns to pending, the reviewer, notes and review timestamp are cleared, and the previously uploaded zip is deleted.

Your published version is never touched by this. A plugin that is live keeps serving its current version throughout review, rejection and resubmission — the only things that change what customers load are a publish and a rollback. So if an approved submission has not been published yet and you spot a bug, just submit the fix: you are not queue-jumping, you are replacing the artefact the reviewer was about to build.

You host nothing

Third-party plugins cannot be self-hosted. Registering a plugin by pointing central at a manifest URL you control still exists, but it is an administrator-only escape hatch for pre-pipeline plugins: it has no dashboard UI at all, and it refuses a slug that already exists. The zip pipeline is the only path open to you.

This matters because the scaffold looks like it disagrees. serve.js, vercel.json and api/ in a freshly generated project are local preview scaffolding — they serve dist/ on your own machine. They are not a deployment target, nothing on the platform consults them, and you can delete them.

Where each step’s rules live

You wantPage
Zip rules, audit gates, size limits, verbatim messagesPackaging and publishing rules
Preflight checklist, the upload UI, review states in detailPreflight and submit
Version rules, propagation, rollbackPublishing, versions and rollback
Install, per-feed config, suspension, kill switchesInstall, customers and kill switches
Which URL a bundle, image or font resolves toKeywords, assets and URLs
The shape decision you can never reverseThe two manifest shapes
Every number and message in one indexLimits and error index