Extension points enable plugins to provide pluggable implementations for core functionality. They follow the Strategy Pattern, allowing different implementations to be swapped at runtime.
If your custom authentication strategy creates new user accounts automatically (e.g., LDAP, SSO, or custom OAuth implementations), you must check the platform’s registration settings before creating users.
Use the typed RPC client to call auth-backend.getRegistrationStatus() and verify that allowRegistration is true before creating any new users. If registration is disabled, throw an appropriate error.
"Registration is disabled. Please contact an administrator."
);
}
// Proceed with user creation
} catch (error) {
logger.warn("Failed to check registration status:", error);
throw error;
}
},
});
This ensures administrators have full control over user registration across all authentication methods. See Backend Service Communication for more details on using the RPC client.
Plugins can expose their own slots using the createSlot utility from @checkstack/frontend-api. This allows other plugins to extend specific areas of your plugin’s UI.
The catalog browse view surfaces a group-level health rollup without depending on any health provider. It does this through the optional CatalogBrowseHealthSlot contract: catalog only consumes the slot, and a health provider plugin fills it.
The slot context passes the visible system ids and a callback the filler reports statuses to:
The filler renders nothing visible. It is a headless data boundary that bulk-fetches health for systemIds and reports the resolved statuses via onStatuses.
CatalogHealthStatus is catalog’s own vocabulary. A filler maps its own status enum into these three values so catalog stays decoupled from the provider’s types.
A system absent from the reported map is treated as "unknown" by the catalog rollup, never as healthy. This matters because healthy systems emit no per-system badge, so “all healthy” can only be derived from the reported data, not from rendered output.
When the slot is unfilled (no health provider installed), group headers show member counts only and the health filter is disabled. Catalog remains fully functional.
Per-system badges continue to come from SystemStateBadgesSlot; this slot exists only to feed the group-level rollup and the health filter from the underlying status data.
Example filler (the health-provider side owns all cross-plugin coupling):
The catalog browse view AND the catalog management systems table mount small
contributions on every system row - state badges (SystemStateBadgesSlot), a
notification bell, and so on. Rendered naively, each one fetches its own data, so
a catalog with N systems issues O(N) requests on open (an N+1). This slot removes
that without coupling catalog to any provider: catalog only renders the
boundary (around the browse tree and around the manage systems table), and a
provider plugin fills it with a component that wraps the tree in a bulk-data
provider. The manage table surfaces no group rows, so it passes an empty
groupIds; a filler’s group-badge provider then fetches nothing there.
systemIds:string[]; // every system id currently rendered in the browse view
groupIds:string[]; // every real group id rendered (excludes the ungrouped section)
}
export const CatalogBrowseDataBoundarySlot =
createSlot<CatalogBrowseDataBoundarySlotContext>(
"plugin.catalog.browse-data-boundary",
);
Contract rules:
A filler is an eager component that receives the context plus React
children (type children as optional so the component stays assignable to
the slot context, which does not declare it) and MUST render childrenexactly once inside its provider. The per-row contributions inside then
read bulk data from that provider’s React context and issue no per-row request.
catalog-frontend folds every registered filler around the tree, so multiple
providers nest and the tree still renders exactly once. Nesting order between
fillers is irrelevant (each provides its own context).
When no filler is installed, the boundary renders the tree unchanged and each
contribution falls back to its own fetch - catalog stays fully functional.
Keep the filler a pure data provider (no visible output of its own).
Example filler (dashboard-frontend wraps its existing bulk badge-data provider):
The dashboard overview lists only the systems that need attention and hides
healthy ones. It builds that list entirely from signals reported through the
SystemSignalsSlot contract, so it is agnostic to which plugins contribute:
the dashboard only consumes the slot, and any plugin (including third-party
plugins) fills it to add a new kind of per-system state to the overview. Adding
a new signal source requires no dashboard change.
A signal carries everything the overview needs to surface, sort, count, and
deep-link the issue:
The filler renders nothing visible. It is a headless data boundary that
bulk-fetches its state for systemIds (no N+1) and reports a per-system-id
signal map via onSignals, tagged with its own stable sourceId.
Re-reporting with the same sourceIdreplaces that source’s previous
contribution, so a source that reports an empty map clears its signals.
A system absent from every source’s map has no signals and is hidden from
the overview (it is healthy). The dashboard derives the “all healthy” state,
the severity counts, and the sort order purely from the reported DATA, never
from rendered output.
Sort order is worst tone first (error -> warn -> info), matching the
icon-only StatusBadge ordering used elsewhere.
Set accessRule whenever href points at a permission-gated page. The
dashboard renders the signal as a LINK only if the current user satisfies that
rule, and as plain TEXT otherwise - so a user is never offered a deep link that
would immediately hit “Access Denied”. Omit accessRule only when the target
needs no specific permission (the link is then always rendered).
Each core reliability plugin (healthcheck, incident, SLO, maintenance, anomaly,
dependency) ships a filler for this slot. A third-party plugin adds a new signal
type the same way:
The About Checkstack page (@checkstack/about-frontend) is a general,
platform-owned surface, so it must not depend on any specific plugin. It exposes
AboutSectionsSlot (from @checkstack/about-common) for plugins to CONTRIBUTE
self-contained section cards; the About page only renders whatever is registered.
This inverts the dependency: the owning plugin imports @checkstack/about-common,
never the other way round.
Each contribution is fully self-contained: it renders its own card and gates
itself (returning null when the viewer lacks the relevant access rule), so
it never fires a request the viewer is not permitted to make.
Extensions declare an optional priority in their metadata; the page renders
them sorted ascending (lower first), mirroring DashboardSlot. Unspecified
priority defaults to 0.
Every extension provides exactly one of component or load:
component (eager) - bundled with the plugin and registered at load. Use for
LIGHT, always-rendered contributions (navbar items, user-menu links, status
badges) where code-splitting would only add a load flash.
load (lazy) - a () => import(...).then((m) => ({ default: m.X })) thunk.
The framework renders it through React.lazy inside a Suspense boundary and a
per-plugin error boundary, so its chunk is fetched on demand and a failed load
is contained to that one contribution. Use for HEAVY or page-scoped
contributions (dashboards, editors, chart panels).
createSlotExtension(SystemEditorSlot, {
id: "myplugin.system-editor",
// Heavy editor → lazy; only loads when the editor slot renders.
If you read extensions yourself via useSlotExtensions (e.g. to build a tab
bar) instead of <ExtensionSlot>, render each one with the <ExtensionComponent extension={ext} context={...} /> helper so both eager and lazy contributions
are handled uniformly.
Some slots need each extension to declare a static descriptor at registration
time - for example, the Infrastructure Settings tab bar needs a label, icon,
and access rules to render its nav before the tab body is mounted. Pass a
second type argument to createSlot to express that contract:
// tabs[i].metadata is typed as InfrastructureTabMetadata
<ExtensionSlot slot={…} context={…} /> remains the right tool when the
consumer just needs to render every extension inline. Reach for
useSlotExtensions only when you need metadata, ordering, or per-extension
gating logic.
Extensions render below the profile header, ordered by their optional
priority (ascending, lower first). Extensions that omit it default to 0
and keep their registration order, which depends on plugin load order - so
declare a priority whenever position matters. The core contributions leave
gaps for third-party plugins to slot between them:
Priority
Contribution
-10
Help (tips-frontend)
10 / 20 / 30
Theme, Low Power, Density (theme-frontend)
40
About (about-frontend)
100
Logout (auth-frontend)
To render a visual divider above your section, emit a DropdownMenuSeparator
as your component’s first child.