Pilets
A pilet is a self-contained JavaScript module: independently developed, independently deployed, loaded at runtime by the app shell. It has one public interface — its setup function — through which it registers everything it contributes.
The setup function
The shell calls setup once when the pilet loads. After it returns, all registrations are live.
setup can be async. The shell waits for the promise to resolve:
The teardown function
Alongside the required setup, a pilet may export an optional teardown(api). The shell calls it when the pilet is removed at runtime — most commonly when it is hot-reloaded during development, but also when it is updated to a new version or disabled via the feed.
You don't need teardown for anything the shell tracks itself — pages, menus, and extensions are unregistered automatically when the pilet goes away. Use it for custom cleanup the shell can't see: removing DOM/event listeners, closing sockets or timers, or otherwise releasing resources your setup acquired.
What a pilet can register
The exact available methods depend on which plugins the app shell has installed. TypeScript types reflect exactly what's available — no guessing.
Cross-pilet communication
Pilets don't share a module scope. Three mechanisms exist for interaction:
Events
Events don't replay for late-loading pilets. Use them for real-time notifications, not for initial state.
Shared data store
Write to the store early in setup so pilets that load later find the value already there.
Extension slots
For sharing UI across pilet boundaries — one pilet declares a slot, another fills it. See Extension slots for the full story.
Sharing dependencies
Shared dependencies in Piral are not limited to what the app shell declares. Any pilet can declare shared packages via the importmap field in its package.json:
When this pilet loads, recharts and date-fns are placed into side-bundles. If another pilet needs those packages — and they've already been resolved — it reuses the existing instances. No duplicate downloads, no central coordination required.
Central importmap (app shell's package.json): packages are pure externals, always provided by the shell, never bundled by any pilet.
Pilet importmap: creates side-bundles. First pilet to resolve a named package wins; all subsequent pilets reuse it.
Use central sharing for universal packages like React and your design system; use a pilet-level importmap for domain libraries only some teams need. For the full mechanics — externals vs side-bundles, resolution order, and where each dependency belongs — see Dependency sharing.
Pilet lifecycle
Registrations persist for the lifetime of the page session. If the pilet is removed at runtime — hot-reloaded, updated, or disabled via the feed — the shell unregisters everything it tracked and calls the pilet's optional teardown for any custom cleanup. A full page reload clears everything and restarts from step 1.
Building and publishing
Publishing POSTs the packed .tgz to the feed's publish endpoint. The feed stores the bundle on your CDN and records the new version. The next feed response includes it.
In automation (CI/CD) there's no browser, so authenticate with an API key from a secret instead: npx pilet publish --url … --api-key $FEED_API_KEY. See Deploying to production.
Non-React pilets
Official converters exist for Angular, Vue, Svelte, Solid, Preact, Blazor, and more. Set the converter up inside the pilet — import its standalone /convert submodule and wrap your component. No app-shell changes are required:
See Multi-framework pilets for the full setup (and the rare case for registering a converter centrally in the shell).