Skip to content

Pilets ​

A pilet is a packaged micro-frontend module (a .tgz bundle) that an application loads at runtime. The Pilets page of a project is where you upload pilet versions, turn them on and off, download them, and choose which version each application gets.

Where to find it ​

Open a project. In the Project section of the sidebar, click Pilets. The page lists every stored version of every pilet in the project.

The list ​

ColumnWhat it shows
Name / VersionPackage name and version, for example @myapp/dashboard v1.0.0
DescriptionOptional text you set at upload or later
SizeBundle size
HashFirst 8 characters of the SHA-256 hash; hover the chip to see the full value
StatusEnabled or Disabled
Created atUpload time
ActionsDownload, Enable/Disable, Edit, Rollout & rollback, Roll back

Use the Filter drop-down (All, Enabled Only, Disabled Only) and the search box to narrow the list.

Uploading a version ​

  1. Click Upload Pilet.
  2. Fill in Package Name, Version, an optional Description, and choose the Package File.
  3. Click Upload.

Rules the Manager enforces:

  • Name: letters, digits, -, _ and ., up to 214 characters. Scoped names such as @myapp/dashboard are allowed.
  • Version: the form requires major.minor.patch. Use semantic versions.
  • File: .tgz or .tar.gz, at most 50 MiB by default (pilets.max_upload_bytes). A larger file gets 413; an empty file gets 400.
  • Versions are immutable. Uploading a name and version that already exist returns 409. Publish a new version instead.
  • In production the Manager must have a signing key configured (pilets.signing), or it refuses uploads. See Pilet signing and key rotation.

At upload the Manager computes the SHA-256 hash of the bytes and signs them with its Ed25519 key. Both travel with the feed, and the browser checks them before it runs the bundle.

Every upload, enable, disable, description edit and rollout change writes an audit row (PiletPackage or PiletRollout) in the same transaction as the change, recording the bundle's name, version, size, hash and signing key id, never its bytes. See Audit Logs.

What upload changes in Frontend Settings ​

Every upload appends name@version to available_modules in the project's frontend settings and in every tenant and application frontend settings of the project. Entries accumulate; older versions stay listed. Nothing is added to enabled_modules: enabling a pilet for an application is a separate step.

Enabling a pilet for an application ​

Two things decide whether a browser loads a pilet:

  1. The pilet name is listed in enabled_modules of the application's effective frontend settings (project settings, overridden by application settings; a dedicated tenant reads its own tenant settings). Entries are bare names such as @myapp/dashboard, not name@version. The Frontend Settings editor lists the available modules and lets you switch them on. See Settings.
  2. At least one enabled version of that pilet exists in the project.
json
{
  "available_modules": ["@myapp/dashboard@1.0.0", "@myapp/dashboard@1.1.0", "@myapp/analytics@2.1.0"],
  "enabled_modules": ["@myapp/dashboard", "@myapp/analytics"]
}

Which version is served is decided by the rollout state (next section), not by enabled_modules. With no rollout configured, the newest enabled version is served.

Choosing the version: rollout, promote, roll back ​

Click the Rollout & rollback icon (branch icon) on any version of a pilet. The dialog shows the current state and lets you pick a Strategy:

StrategyWhat is served
PinnedThe Stable version to every application
PercentageThe Canary version to a fixed percentage of applications (chosen deterministically per application), the stable version to the rest
CohortThe Canary version to the listed application ids (comma-separated), the stable version to the rest

Click Apply. Changing the stable version records the previous one, so Rollback (in the dialog, or the Roll back icon in the row, which asks you to type the pilet name) re-pins it. Promote canary makes the canary the new stable version and ends the rollout.

If the rollout points at a version that is disabled or missing, the feed serves the newest enabled version instead of a broken link.

A dedicated tenant can also pin a version in its own frontend config: pilets.pins maps a pilet name to a version. A pin wins over the rollout, but only if the pinned version is enabled. See Dedicated tenants.

GraphQL:

graphql
query {
  piletRollout(projectId: "project-uuid", name: "@myapp/dashboard") {
    strategy
    stableVersion
    canaryVersion
    percentage
    cohortIds
    previousVersion
  }
}
graphql
mutation {
  setPiletRollout(
    projectId: "project-uuid"
    name: "@myapp/dashboard"
    strategy: PERCENTAGE
    stableVersion: "1.0.0"
    canaryVersion: "1.1.0"
    percentage: 10
  ) {
    strategy
    stableVersion
    canaryVersion
    percentage
  }
}

promotePiletCanary(projectId, name) and rollbackPilet(projectId, name) take the same two arguments and return the new state.

Editing, disabling, downloading ​

Edit the description ​

Click the pencil icon, change the text, click Save. Only the description can change; the content and version are fixed.

Disable and enable ​

Click the toggle icon in the row. A disabled version is never served and cannot be downloaded through the management API, but it stays in the list and can be enabled again.

Disabling removes exactly name@version from available_modules and enabled_modules in all the project's frontend settings. A bare-name entry in enabled_modules is left alone, so the pilet stays enabled for its other versions.

The REST DELETE route (below) does the same as disabling. Nothing is erased.

Download ​

Click the download icon. The browser saves {name}-{version}.tgz. Use this to back up a bundle or to check its hash:

bash
sha256sum dashboard-1.0.0.tgz

Compare with the full hash shown when you hover the Hash chip.

Access control ​

Everything on this page, in the UI and through the API, needs:

  • Authentication: the browser session from the proxy login, or a Personal Access Token. API calls must go through the proxied entrypoint; the Manager rejects direct calls with 401.
  • Project membership: listing and downloading need any role in the project. Uploading and disabling need any project role except readonly, or the system role administrator. Otherwise: 403 {"error": "access denied to project"}.

The feed the browser reads (/pilet-feed/...) and the bundle route it downloads from (/pilet-bundle/...) take no user credential. They are gated by the proxy's shared secret and only serve what the application already has enabled. See Pilet API.

Hash and signature verification ​

Each stored version carries a SHA-256 hash and an Ed25519 signature over the same bytes. The feed publishes them as hash, signature and keyId. The browser loader verifies both against the public keys the proxy injects as window.__PRODUCTIFY__.config.pilet_signing_keys (never against a key from the feed) and refuses to run a bundle that fails either check.

Console messages and what they mean:

  • Pilet ... has no integrity hash or ... has no provenance signature: the package predates hashing or signing. Upload it again as a new version.
  • Pilet ... is signed by unknown key "<id>": the signing key was rotated and the old public key is not listed under pilets.signing.previous_keys. Add it (public half only) and old bundles verify again. If the key was revoked on purpose, upload the bundles again so the current key signs them. See Pilet signing and key rotation.

Uploading from CI ​

With the CLI ​

bash
pfy pilet upload \
  --project my-project \
  --name @myapp/dashboard \
  --version 1.0.0 \
  --file ./dist/myapp-dashboard-1.0.0.tgz \
  --description "User dashboard"

--project takes the project slug; --project-id takes the id directly. The CLI uses the login saved by pfy login, or the PFY_MANAGER_TOKEN environment variable.

With curl ​

bash
PROJECT_ID="your-project-id"
PAT_TOKEN="your-pat-token"

curl -X POST \
  -H "Authorization: Bearer $PAT_TOKEN" \
  -F "file=@my-pilet-1.0.0.tgz" \
  "https://manager.example.com/pilets/$PROJECT_ID/dashboard/1.0.0?description=User%20dashboard"
  • The multipart field must be named file. The description is an optional query parameter.
  • A scoped name goes in the path percent-encoded: @myapp%2Fdashboard.
  • The URL is the proxied entrypoint you use in the browser.

Success returns 200:

json
{
  "id": "package-uuid",
  "name": "dashboard",
  "version": "1.0.0",
  "size": 123456,
  "hash": "9f8e7d6c...",
  "message": "Package uploaded successfully"
}

409 means the version already exists.

The REST routes are GET /pilets/{projectId} (list; ?enabled=true|false filters), GET /pilets/{projectId}/{name}/{version} (download), POST on the same path (upload) and DELETE on the same path (disable). Details in Pilet API.

Troubleshooting ​

Upload fails ​

Check that the file is under 50 MiB and is .tgz or .tar.gz, that the name uses only allowed characters, that the version does not already exist (409), and, in production, that pilets.signing is configured.

The pilet does not load in the application ​

  1. Its bare name is in enabled_modules for the application (or the project).
  2. At least one version is Enabled.
  3. The rollout does not point at a disabled version. The feed falls back to the newest enabled version, so check that is what you expect.
  4. Read the browser console for a hash or signature message (see above).

Hash mismatch, or HTML where a bundle should be ​

Run curl -s https://<app-host>/pilets/<...> | head -c 200. The rendered application vhost routes /pilets/* to the Manager's bundle route ahead of the application's own catch-all, so HTML there means the proxy is running a configuration that predates the current render or was loaded out of band. Check the proxy panel on the dashboard for drift and push the current render (Sync now or pfy proxy sync).