Architecture overview
Every Piral application has the same three-layer structure. Understanding it makes every other part of the documentation easier to follow.
The three layers
1. The app shell
The app shell is the only thing users download directly from your URL. It is a small, stable, static bundle — typically under 100 KB gzipped — that:
- Bootstraps the React application and global layout
- Calls the feed service to discover which pilets should load for this user
- Fetches and evaluates each pilet's JavaScript bundle
- Calls each pilet's
setup(api)function and hands it the Pilet API - Renders the current route using whatever pages the pilets registered
The app shell knows nothing about individual features. It provides the stage — navigation chrome, authentication context, shared dependencies — and pilets perform on it.
The app shell should change as infrequently as possible. Every app shell deploy potentially affects every user and every pilet team.
2. Pilets
Pilets are the feature modules. Each is a self-contained JavaScript bundle — developed, versioned, and deployed independently by a single team. A pilet has one public interface: its setup function.
Pilets do not communicate directly. All interaction — registering UI, sharing data, emitting events — goes through the Pilet API.
3. The feed service
The feed service is a simple HTTP endpoint. The app shell calls it once at startup and receives a JSON array of pilet metadata:
The feed can apply any logic: serve this pilet only to authenticated users, only to this tenant, only to users in the beta group, only in certain regions. The app shell doesn't care about any of that — it just fetches the URL and loads whatever comes back.
The startup sequence
- Browser loads the app shell — a small, cacheable static bundle
- Shell initialises — React mounts, global layout renders, router starts
- Feed request —
GET /api/v1/piletswith the user's auth token - Bundle loading — each pilet's JS is fetched in parallel from CDN URLs
setup()calls — each pilet registers pages, menus, extensions- Render — router matches the current URL, renders the correct pilet page
Steps 1–5 complete before the user sees any pilet content. This is why pilet bundle count and initial size matter for perceived performance.
Pilets can be marked as lazy in the feed response. Lazy pilets skip steps 4–5 at startup — their bundles are fetched only when the user first navigates to one of their routes. This is the primary performance lever for applications with many pilets.
Shared dependencies
The app shell declares centrally shared packages in its importmap. Pilets treat these as externals and never bundle them.
But any pilet can also share dependencies with other pilets via its own importmap. When a pilet loads a side-bundled package, that package becomes available to all subsequently loaded pilets that declare the same name. The first pilet to resolve a dependency wins; others reuse it.
This distributed sharing model means teams don't need to coordinate with the app shell team to share a domain library. The team that introduces the library owns the sharing declaration.
The development loop
The emulator closes the gap between local development and the full platform:
See Emulator for the complete story.