Upload third-party extension bundles, review scopes, activate, roll back.
Extensions (Install & Manage)
This page is for the tenant admin installing an extension someone else built. For the author's side — scaffolding, iterating, publishing — head to the Extension Authoring recipe.
Upload flow
From /admin/extensions:
- Click Upload.
- Pick a
.zipcontainingmanifest.json+dist/bundle.mjs. - The three-layer security scanner runs (MIME magic → VirusTotal hash lookup → LLM content inspection). A Malicious verdict rejects the upload outright. Suspicious / Skipped pass with a visible badge on the row.
- On success the extension is installed and active
(
is_active = 1).
SHA-256 of the bundle is recorded alongside the row so you can cross-check against the author's published checksum.
Reviewing scopes before activation
Every manifest declares required_scopes, e.g.
["entities:read", "jobs:read"]. The admin shell shows the
declared set prominently in the upload confirmation dialog.
Reject the upload if the scopes don't match what you expect from
the extension's documented purpose.
See the full scope vocabulary in the Auth page.
Versioning
Uploads are keyed by (tenant_id, extension_id, version). The
important rules:
- Same
(extension_id, version)uploaded twice → 409 Conflict. You can't overwrite history. - Different version of the same
extension_id→ the previous active row flips tois_active = 0; the new one lands active. Only one version of a given extension is active at a time per tenant. - Historical rows stay in the DB forever — the ledger is your rollback source.
The version ledger is append-only. There is no "delete this
version" action; uninstall below flips is_active = 0 but keeps
the row.
Rolling back
The detail view for an extension id shows every uploaded version
with its created_at, actor, and scan verdict. To roll back:
- Click the desired historical version.
- Activate — flips the current row to
is_active = 0and the selected one tois_active = 1in one transaction. - The UI reloads pointing at the newly-active bundle.
No bytes are re-uploaded; the blob in the old row is served
directly from tenant_extensions.bundle_blob.
Uninstalling
"Uninstall" means setting is_active = 0 on every row for the
extension id. The bundle blobs stay in the DB (for potential
future re-activation + audit). If you need to remove the bytes
outright, that's a platform-admin operation — not exposed in
the tenant admin shell.
What an installed extension can do
Depending on the manifest:
- Pages — mount at
/ext/:ext_id/<path>. Show up in the admin nav under an Extensions heading. - Components — custom block types available in the Page Designer palette.
Every extension runs inside a
FastYokeProvider with a short-lived
15-minute JWT carrying ext_id + scopes. The host provides
react, react-dom, and @fastyoke/sdk via the import map so
every extension shares one React instance + one SDK client with
the shell — context-based state (current user, tenant, feature
flags) crosses cleanly.
Troubleshooting
| Symptom | Cause |
|---|---|
| Upload rejected: "bundle rejected by security scan" | Scanner flagged the bundle. Ask the author to re-scan locally + compare; often a third-party dep with heuristic-positive code. |
| Component renders blank | Host/extension React instance mismatch. See the authoring recipe's dev-mode note. |
| 409 on re-install | Version already uploaded. Ask the author to bump manifest.json → version. |
Related
- Extension Authoring — the author's side of the same contract.
- SDK Reference — the hooks + components an installed extension can use.
- Authentication → extension JWT — the token shape that backs an installed extension's API calls.