Documentation source
Plugins
One admin surface to discover, install, author, and uninstall capability packs — built on the BundleManifest engine.
## Overview
A **Plugin** is an installable capability pack that overlays a workspace or tenant with ready-made functionality: data types, views, agents, tasks, criteria sets, pages, navigation, skills, theme tokens, and seed data. Installing a plugin fans its contents into the platform's existing tables in a single Postgres transaction and stamps every installed row with provenance so the pack can be audited and uninstalled cleanly later.
The user-facing surface is **Admin > Plugins** (`/admin/plugins`), which is the sole place to browse, install, uninstall, and author plugins. It is **admin-only**, gated on the `workspaces.team.manage` permission.
**Architectural decisions:**
- Plugins are additive overlays, not fork replacements — see [ADR-0007](/docs/adr/0007-plugin-bundles-as-overlays)
- The install boundary is the workspace, not the tenant — see [ADR-0008](/docs/adr/0008-workspaces-as-install-boundary)
- This consolidation replaced the three parallel install surfaces that preceded it — see [ADR-0058](/docs/adr/0058-plugins-unified-install-surface)
**The "Plugin" noun is user-facing only.** The underlying engine keeps its existing vocabulary: `BundleManifest`, `bundle_installations`, `install_bundle()`. "Plugin" is the presentation label. Agents and engineers working at the engine level use the bundle terminology.
**Mini-apps are different.** A Mini-app (`/views` → Mini-apps tab) is a single published interactive view. A Plugin is an installable capability pack. They differ in size, verb ("publish" vs "install"), and location.
## What a Plugin Can Contain
A plugin manifest (`BundleManifest`) can declare any combination of these sections — all optional:
| Section | Installs into |
| --------------- | ------------------------------------------- |
| `entityTypes` | `public.entity_types` (data types) |
| `agents` | `public.agents` |
| `tasks` | `public.actions` (action registry) |
| `tools` | runtime registration via `registerTool()` |
| `views` | `public.views` |
| `blocks` | runtime registration via `registerBlock()` |
| `nav` | merged into `nav_config` jsonb |
| `skills` | runtime registration via `registerSkill()` |
| `theme` | merged into `tenant_settings.theme_tokens` |
| `criteriaSets` | `public.criteria_sets` |
| `seedData` | initial rows in `entities` + `entity_relations` |
| `custom_pages` | `public.custom_pages` (static pages only) |
| `workspace` | creates a new workspace when installed |
| `permissions` | permission declarations for the pack |
> **Known limitation (v1):** Module-runtime custom pages (`custom_pages.runtime = 'module'`) and agent-authored generative blocks (`ui_artifacts`-backed) cannot be packaged in a plugin. Static pages and code/registry blocks are in scope. This is a v1 constraint; cross-tenant social publishing ("marketplace") is also deferred to v2.
## The Admin Surface (`/admin/plugins`)
The page has two tabs and a **Create plugin** button:
- **Browse** — discover available plugins (Product, Template, and Authored sources) and install them. Default landing.
- **Installed** — audit what is installed in the current scope; uninstall from here or from any plugin's detail sheet.
- **Create plugin** (button, top-right) — opens the authoring wizard.
### Plugin Sources and the Card Grid
Each plugin card shows the plugin's display name, a **source badge** (`Product` / `Template` / `Authored`), "contains" chips (N data types · N views · N agents · N tasks), version, and an `Installed` badge when relevant.
| Source | Description |
| --------- | --------------------------------------------------------------------------------------------------- |
| `Product` | First-party plugins shipped in `features/custom/bundles/` via the `BUNDLE_CATALOG` |
| `Template` | Code-defined `WorkspaceTemplate` entries from `features/workspaces/templates.ts` |
| `Authored` | Plugins you authored in-browser via the Create wizard and saved to `authored_bundle_manifests` |
### Installing a Plugin
Two install variants exist, distinguished visually on the card:
**Overlay install** — installs a plugin into the current workspace. Install button is solid primary. Low-consequence and reversible. No confirm step.
**Workspace-creating install** — installs a template that creates a new workspace. Identified by a workspace glyph and "Creates a new workspace" subtitle; install button is outline-styled. Requires a confirm step because the result (a new workspace and navigation away) is heavier and less reversible.
### Uninstalling a Plugin
Uninstall is available from the Installed tab and from any installed plugin's detail sheet. A confirm dialog shows what will be removed and what will be kept:
- Views, tasks, and owned agents are removed.
- Data types that still hold records are kept (orphan-safe by default).
- The workspace created by a workspace-creating install is **not** deleted on uninstall — remove it separately via Workspace settings.
## Provenance and Cascade Behavior
Every row installed by a plugin is stamped with:
| Column | Meaning |
| ---------------------------- | ------------------------------------------------------------------ |
| `installed_by_bundle` | Bundle name (`@amble/…` or `@<tenant>/…`) |
| `bundle_version` | Semver string at install time |
| `installed_by_workspace_id` | UUID of the workspace scope, or `NULL` for tenant-scoped sections |
The `uninstall_bundle()` SQL function deletes owned rows. By default it orphans rows that are still referenced by other data (safe). Two optional cascade flags — `p_delete_content` (delete all installed content) and `p_delete_owned_type_records` (delete records whose type was installed by the bundle) — default to `false` to minimize accidental data loss.
## Two Authoring Doors, One Shape
Both authoring doors produce a `BundleManifest` and install via `install_bundle()`. The door is a source detail; the engine is the same.
### Door 1 — Code-Defined (via `TenantModule`)
Product plugins are authored in code: a `TenantModule` (or `TenantDeclarationsModule`) in `features/custom/tenants/<slug>/` declares entity types, agents, views, criteria sets, and actions. The bootstrap layer synthesizes these into a `BundleManifest` that feeds the engine at install time.
This is the path for first-party ("Product") plugins.
### Door 2 — Browser-Authored (the Create Wizard)
The **Create plugin** button opens a two-step wizard:
1. **Build** — give the plugin a display name and select existing DB artifacts (data types, views, agents, criteria sets, tasks, static pages). Obvious cross-artifact dependencies are auto-included with an undo affordance (a view that references a data type pulls the data type in automatically, shown as a chip you can remove).
2. **Preview** — the server runs `serializeTenantArtifactsToManifest()`, which normalizes all embedded UUIDs to slugs (using the same `fromRow` adapters as tenant-sync), strips non-serializable fields (`queue_config`, module-runtime pages, agent-authored blocks), and validates the result through `parseBundleManifest()` with `.strict()`. You see counts, any warnings (out-of-selection refs that were stripped), and a "Manifest valid" confirmation when the output is UUID-clean.
- **Save** — persists to `authored_bundle_manifests` with `status: 'ready'` (if the manifest parses) or `status: 'draft'` (if not). The card appears in Browse under the Authored filter.
- **Save & install** — saves and immediately installs into the current scope.
**Package naming:** the package name is auto-derived as `@<tenant-slug>/<slugified-display-name>`. Authored plugins cannot impersonate product `@amble/` packages. Version starts at `1.0.0` and auto-bumps on subsequent saves of the same row.
**Editing:** an authored plugin can be re-edited at any time. The wizard reopens pre-populated from the stored `ArtifactSelection` (not reverse-engineered from the manifest), then re-serializes from live DB state.
**Export:** a plugin's manifest JSON can be downloaded via the overflow menu on its detail sheet. The file is installable in any compatible Amble tenant via the API.
### The UUID-clean Hard Invariant
The serializer never emits source-workspace UUIDs into the manifest. Every embedded UUID is resolved to a slug; refs outside the selection are stripped with a warning. `parseBundleManifest()` with `.strict()` is the backstop: if a UUID leaks, the parse fails and the error surfaces in the Preview step — never a silently broken plugin.
## Engine Internals (for engineers and agents)
The engine lives in `features/bundles/`. The Plugins UI layer lives in `features/plugins/`.
```
AUTHOR (two doors) ONE ENGINE INSTALL
code: TenantModule ──▶ BundleManifest (manifest.ts) ──▶ install_bundle() SQL (1 txn)
declarations ──▶ ▲ ├─ entity_types
(bootstrap synthesis) │ serializeTenantArtifacts ├─ agents
│ ToManifest() ◀── DB artifacts ├─ views / tasks
browser: create wizard ──▶ │ (reuses tenant-sync fromRow) ├─ criteria sets / pages / nav
BUNDLE_CATALOG (discovery) └─ bundle_installations (provenance)
│
/admin/plugins (ONE admin surface)
browse · detail · install · uninstall · create
```
Key files:
| Path | Role |
| ------------------------------------------------ | ------------------------------------------------------------------- |
| `features/bundles/lib/manifest.ts` | `BundleManifest` Zod schema; `parseBundleManifest()` |
| `features/bundles/server/install-bundle.ts` | `installBundle()` — Zod parse → collision lint → SQL RPC |
| `features/bundles/server/serialize.ts` | `serializeTenantArtifactsToManifest()` — the browser-authoring backend |
| `features/bundles/server/catalog.ts` | `listAvailablePlugins()` — merges product + template + authored |
| `features/custom/bundles/index.ts` | `BUNDLE_CATALOG` — product plugin registry |
| `features/workspaces/templates.ts` | `WorkspaceTemplate` definitions (code-defined workspace installs) |
| `features/workspaces/server/install.ts` | `installWorkspaceFromTemplate()` — the workspace-creating path |
| `supabase/migrations/20260427080000_bundle_installer.sql` + later migrations | `install_bundle()` SQL function |
Database tables owned by the engine:
| Table | Purpose |
| ---------------------------- | ----------------------------------------------------------------------- |
| `bundle_installations` | One row per installed plugin per scope; tracks version + status |
| `authored_bundle_manifests` | Browser-authored plugin drafts and published manifests |
## For Agents
When an agent is asked to install a capability pack or set up a workspace with a specific configuration:
1. Call `listAvailablePlugins(ctx)` to get the merged catalog.
2. Surface the relevant plugin to the user (Browse tab or name the plugin).
3. On confirmation, `POST /api/plugins` with `{ action: 'install', source: 'manifest', manifestName, workspaceId }` (overlay) or `{ action: 'install', source: 'template', templateSlug }` (workspace-creating).
To author a new plugin from existing DB artifacts:
1. Call `listAuthorableArtifacts(tenantId)` to get available artifacts and the reference graph.
2. Let the user select artifacts.
3. Call `serializeTenantArtifactsToManifest(tenantId, selection)` to preview the manifest and warnings.
4. `POST /api/plugins` with `{ action: 'create', selection, displayName }` to save.
Agents should never author a new `BundleManifest` by hand for browser-initiated flows; use the serialize function which enforces the UUID-clean invariant. For code-defined product plugins, author a `TenantModule` in `features/custom/tenants/<slug>/`.
## v1 / v2 Boundary
**v1 (current):**
- Install, uninstall, and author plugins within the same tenant's workspaces
- Export a manifest JSON for version control or manual install elsewhere
- Admin-only surface (`workspaces.team.manage`)
**v2 (deferred):**
- Cross-tenant publish and discovery ("social marketplace")
- Plugin upgrade/version-diff flow
- Module-runtime custom pages and agent-authored blocks in authored plugins
- Per-plugin `plugins.manage` permission (v1 reuses `workspaces.team.manage`)
## Related Modules
- [Agent System](/docs/features/agent-system) — plugins seed agents and assign role permissions
- [Tasks](/docs/features/tasks) — plugins seed action registry rows
- [View System](/docs/features/view-system) — plugins ship view configs
- [Block System](/docs/features/block-system) — plugins register custom block components
- [Multi-tenant](/docs/features/multi-tenant) — plugins install per-tenant via RLS / GUC
- [Workspaces](/docs/features/workspaces) — the install boundary; workspace-creating installs add a workspace row
- [Customization Map](/docs/features/customization-map) — where plugins fit in the broader customization model