Merge branch 'backstage:master' into feature/catalog-export

This commit is contained in:
1337
2026-05-20 20:32:27 +02:00
committed by GitHub
1172 changed files with 67648 additions and 10161 deletions
@@ -60,7 +60,7 @@ Even with feature discovery enabled, you can disable specific extensions via con
app:
extensions:
- page:techdocs: false
- nav-item:search: false
- page:search: false
```
### How Discovery Works with Manual Imports
+5
View File
@@ -19,6 +19,11 @@
"name": "plugin-full-frontend-system-migration",
"description": "Fully migrate a Backstage plugin to the new frontend system, dropping all old system support. Use this skill for internal plugins that only need to run in a single app, or when you are ready to remove backward compatibility entirely.",
"files": ["SKILL.md"]
},
{
"name": "plugin-analytics-instrumentation",
"description": "Instrument a Backstage frontend plugin with analytics events using the Backstage Analytics API. Use this skill when adding, reviewing, or extending event capture (captureEvent, AnalyticsContext) in plugin components, deciding whether an interaction warrants an event, or writing tests for analytics behavior.",
"files": ["SKILL.md"]
}
]
}
@@ -5,8 +5,8 @@ description: Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (B
# MUI to BUI Migration Skill
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to Backstage UI (
@backstage/ui).
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to
Backstage UI (@backstage/ui).
## Prerequisites
@@ -19,6 +19,7 @@ Before starting migration:
```
2. Add the CSS import to your root file (typically `src/index.ts` or app entry point):
```typescript
import '@backstage/ui/css/styles.css';
```
@@ -38,11 +39,14 @@ Before starting migration:
- `Accordion` - Collapsible content panels (`Accordion`, `AccordionTrigger`, `AccordionPanel`, `AccordionGroup`)
- `Alert` - Alert/notification banners (`status`, `title`, `description`)
- `Avatar` - User/entity avatars
- `Badge` - Inline badge/label with optional icon (`size`, `icon`)
- `Button` - Action buttons (`variant="primary"`, `variant="secondary"`, `variant="tertiary"`, `isDisabled`, `destructive`, `loading`)
- `ButtonIcon` - Icon-only buttons (`icon`, `onPress`, `variant`)
- `ButtonLink` - Link styled as button
- `Card` - Content cards (`Card`, `CardHeader`, `CardBody`, `CardFooter`)
- `Checkbox` - Checkbox input
- `CheckboxGroup` - Grouped checkboxes with shared label (`label`, `orientation`, `isRequired`)
- `DateRangePicker` - Date range input field (`label`, `value`, `onChange`)
- `Dialog` - Modal dialogs (`DialogTrigger`, `Dialog`, `DialogHeader`, `DialogBody`, `DialogFooter`)
- `FieldLabel` - Form field label with description and secondary label
- `Header` - Page headers with breadcrumbs and tabs
@@ -57,6 +61,7 @@ Before starting migration:
- `SearchField` - Search input
- `Select` - Dropdown select (single and multiple selection modes)
- `Skeleton` - Loading skeleton
- `Slider` - Range slider input (`label`, `minValue`, `maxValue`, `step`)
- `Switch` - Toggle switch
- `Table` - Data tables (with `useTable` hook for data management)
- `TablePagination` - Standalone pagination component
@@ -103,9 +108,9 @@ Create a `.module.css` file alongside your component using BUI CSS variables.
**Before (MUI `makeStyles`):**
```typescript
```tsx
// MyComponent.tsx
import {makeStyles, Theme} from '@material-ui/core/styles';
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
container: {
@@ -130,18 +135,16 @@ const useStyles = makeStyles((theme: Theme) => ({
function MyComponent() {
const classes = useStyles();
return (
<div className = {classes.container} >
<Typography className = {classes.title} > Title < /Typography>
< div
className = {classes.listItem} >
<div className = {classes.icon} >
<SomeIcon / >
<div className={classes.container}>
<Typography className={classes.title}>Title</Typography>
<div className={classes.listItem}>
<div className={classes.icon}>
<SomeIcon />
</div>
<span>Content</span>
</div>
</div>
< span > Content < /span>
< /div>
< /div>
)
;
);
}
```
@@ -177,27 +180,24 @@ function MyComponent() {
}
```
```typescript
```tsx
// MyComponent.tsx
import {Box, Text} from '@backstage/ui';
import {RiSomeIcon} from '@remixicon/react';
import { Box, Text } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
import styles from './MyComponent.module.css';
function MyComponent() {
return (
<Box className = {styles.container} >
<Text className = {styles.title} > Title < /Text>
< div
className = {styles.listItem} >
<div className = {styles.icon} >
<RiSomeIcon size = {24}
/>
< /div>
< span > Content < /span>
< /div>
< /Box>
)
;
<Box className={styles.container}>
<Text className={styles.title}>Title</Text>
<div className={styles.listItem}>
<div className={styles.icon}>
<RiSomeIcon size={24} />
</div>
<span>Content</span>
</div>
</Box>
);
}
```
@@ -205,38 +205,27 @@ function MyComponent() {
**Before (MUI Box with display prop):**
```typescript
```tsx
<Box
display = "flex"
flexDirection = "column"
alignItems = "center"
justifyContent = "space-between"
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="space-between"
>
<Box display = "flex"
flexDirection = "row"
gap = {2} >
{children}
< /Box>
< /Box>
<Box display="flex" flexDirection="row" gap={2}>
{children}
</Box>
</Box>
```
**After (BUI `Flex` component):**
```typescript
<Flex direction = "column"
align = "center"
justify = "between" >
<Flex direction = "row"
style = {
{
gap: 'var(--bui-space-4)'
}
}>
{
children
}
```tsx
<Flex direction="column" align="center" justify="between">
<Flex direction="row" style={{ gap: 'var(--bui-space-4)' }}>
{children}
</Flex>
</Flex>
< /Flex>
```
Note: BUI `Flex` uses `justify="between"` not `justify="space-between"`.
@@ -245,70 +234,40 @@ Note: BUI `Flex` uses `justify="between"` not `justify="space-between"`.
**Before (MUI Grid):**
```typescript
<Grid container
spacing = {3} >
<Grid item
xs = {12}
md = {6} >
{content}
< /Grid>
< /Grid>
```tsx
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
{content}
</Grid>
</Grid>
```
**After (BUI Grid):**
```typescript
<Grid.Root columns = {
{
sm: '12'
}
}
gap = "6" >
<Grid.Item colSpan = {
{
sm: '12', md
:
'6'
}
}>
{
content
}
</Grid.Item>
< /Grid.Root>
```tsx
<Grid.Root columns={{ sm: '12' }} gap="6">
<Grid.Item colSpan={{ sm: '12', md: '6' }}>{content}</Grid.Item>
</Grid.Root>
```
### 5. Typography to Text
**Before (MUI Typography):**
```typescript
<Typography variant = "h1" > Heading < /Typography>
< Typography
variant = "h6" > Subheading < /Typography>
< Typography
variant = "body1" > Body
text < /Typography>
< Typography
variant = "body2"
color = "textSecondary" > Secondary
text < /Typography>
```tsx
<Typography variant="h1">Heading</Typography>
<Typography variant="h6">Subheading</Typography>
<Typography variant="body1">Body text</Typography>
<Typography variant="body2" color="textSecondary">Secondary text</Typography>
```
**After (BUI Text):**
```typescript
<Text variant = "title-large" > Heading < /Text>
< Text
variant = "title-small" > Subheading < /Text>
< Text
variant = "body-medium" > Body
text < /Text>
< Text
variant = "body-small"
color = "secondary" > Secondary
text < /Text>
```tsx
<Text variant="title-large">Heading</Text>
<Text variant="title-small">Subheading</Text>
<Text variant="body-medium">Body text</Text>
<Text variant="body-small" color="secondary">Secondary text</Text>
```
Valid Text variants: `title-large`, `title-medium`, `title-small`, `title-x-small`, `body-large`, `body-medium`,
@@ -318,19 +277,17 @@ Valid Text variants: `title-large`, `title-medium`, `title-small`, `title-x-smal
**Before (MUI Tooltip):**
```typescript
import {Tooltip, Typography} from '@material-ui/core';
```tsx
import { Tooltip, Typography } from '@material-ui/core';
<Tooltip title = { < Typography > Tooltip
content < /Typography>}>
< span > Hover
me < /span>
< /Tooltip>;
<Tooltip title={<Typography>Tooltip content</Typography>}>
<span>Hover me</span>
</Tooltip>;
```
**After (BUI TooltipTrigger pattern):**
```typescript
```tsx
import { Tooltip, TooltipTrigger, Text } from '@backstage/ui';
<TooltipTrigger>
@@ -343,26 +300,23 @@ import { Tooltip, TooltipTrigger, Text } from '@backstage/ui';
**Before (MUI Dialog):**
```typescript
import {Dialog, DialogTitle, DialogActions, Button} from '@material-ui/core';
```tsx
import { Dialog, DialogTitle, DialogActions, Button } from '@material-ui/core';
<Dialog open = {isOpen}
onClose = {onClose} >
<DialogTitle>Title < /DialogTitle>
< DialogActions >
<Button onClick = {onClose} > Cancel < /Button>
< Button
onClick = {onConfirm}
color = "primary" >
Confirm
< /Button>
< /DialogActions>
< /Dialog>;
<Dialog open={isOpen} onClose={onClose}>
<DialogTitle>Title</DialogTitle>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onConfirm} color="primary">
Confirm
</Button>
</DialogActions>
</Dialog>;
```
**After (BUI Dialog):**
```typescript
```tsx
import {
Dialog,
DialogTrigger,
@@ -373,78 +327,58 @@ import {
<DialogTrigger>
<Dialog
isOpen = {isOpen}
isDismissable
onOpenChange = {open
=>
{
if (!open) onClose();
}
}
>
<DialogHeader>Title < /DialogHeader>
< DialogFooter >
<Button onClick = {onConfirm}
variant = "primary" >
Confirm
< /Button>
< Button
onClick = {onClose}
variant = "secondary"
slot = "close" >
Cancel
< /Button>
< /DialogFooter>
< /Dialog>
< /DialogTrigger>;
isOpen={isOpen}
isDismissable
onOpenChange={open => {
if (!open) onClose();
}}
>
<DialogHeader>Title</DialogHeader>
<DialogFooter>
<Button onClick={onConfirm} variant="primary">
Confirm
</Button>
<Button onClick={onClose} variant="secondary" slot="close">
Cancel
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>;
```
### 8. Button Changes
**Before (MUI Button):**
```typescript
<Button variant = "contained"
color = "primary"
disabled = {loading}
onClick = {handleClick} >
```tsx
<Button variant="contained" color="primary" disabled={loading} onClick={handleClick}>
Submit
< /Button>
< IconButton
onClick = {handleDelete}
disabled = {!
canDelete
}>
<DeleteIcon / >
</Button>
<IconButton onClick={handleDelete} disabled={!canDelete}>
<DeleteIcon />
</IconButton>
```
**After (BUI Button):**
```typescript
<Button variant = "primary"
isDisabled = {loading}
onClick = {handleClick} >
```tsx
<Button variant="primary" isDisabled={loading} onClick={handleClick}>
Submit
< /Button>
< ButtonIcon
aria - label = "delete"
isDisabled = {!
canDelete
}
onPress = {handleDelete}
icon = { < RiDeleteBinLine
size = {16}
/>}
variant = "secondary"
/ >
</Button>
<ButtonIcon
aria-label="delete"
isDisabled={!canDelete}
onPress={handleDelete}
icon={<RiDeleteBinLine size={16} />}
variant="secondary"
/>
```
### 9. TextField Changes
**Before (MUI TextField):**
```typescript
```tsx
<TextField
required
name="title"
@@ -457,7 +391,7 @@ variant = "secondary"
**After (BUI TextField):**
```typescript
```tsx
<TextField
isRequired
id="title"
@@ -473,89 +407,72 @@ Note: BUI TextField `onChange` receives the string value directly, not an event
**Before (MUI Tabs):**
```typescript
import {Tab} from '@material-ui/core';
import {TabContext, TabList, TabPanel} from '@material-ui/lab';
```tsx
import { Tab } from '@material-ui/core';
import { TabContext, TabList, TabPanel } from '@material-ui/lab';
<TabContext value = {tab} >
<TabList onChange = {handleChange} >
<Tab label = "Tab 1"
value = "tab1" / >
<Tab label = "Tab 2"
value = "tab2" / >
<TabContext value={tab}>
<TabList onChange={handleChange}>
<Tab label="Tab 1" value="tab1" />
<Tab label="Tab 2" value="tab2" />
</TabList>
< TabPanel
value = "tab1" > Content
1 < /TabPanel>
< TabPanel
value = "tab2" > Content
2 < /TabPanel>
< /TabContext>;
<TabPanel value="tab1">Content 1</TabPanel>
<TabPanel value="tab2">Content 2</TabPanel>
</TabContext>;
```
**After (BUI Tabs):**
```typescript
import {Tabs, TabList, Tab, TabPanel} from '@backstage/ui';
```tsx
import { Tabs, TabList, Tab, TabPanel } from '@backstage/ui';
<Tabs defaultSelectedKey = "tab1" >
<TabList>
<Tab id = "tab1" > Tab
1 < /Tab>
< Tab
id = "tab2" > Tab
2 < /Tab>
< /TabList>
< TabPanel
id = "tab1" > Content
1 < /TabPanel>
< TabPanel
id = "tab2" > Content
2 < /TabPanel>
< /Tabs>;
<Tabs defaultSelectedKey="tab1">
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
</TabList>
<TabPanel id="tab1">Content 1</TabPanel>
<TabPanel id="tab2">Content 2</TabPanel>
</Tabs>;
```
### 11. Menu Pattern
**Before (MUI Menu):**
```typescript
```tsx
import {IconButton, Popover, MenuList, MenuItem} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
<IconButton onClick = {handleOpen} > <MoreVertIcon / > </IconButton>
< Popover
open = {open}
anchorEl = {anchorEl}
onClose = {handleClose} >
<MenuList>
<MenuItem onClick = {handleAction} > Action < /MenuItem>
< /MenuList>
< /Popover>
<IconButton onClick={handleOpen}>
<MoreVertIcon />
</IconButton>
<Popover open={open} anchorEl={anchorEl} onClose={handleClose}>
<MenuList>
<MenuItem onClick={handleAction}>Action</MenuItem>
</MenuList>
</Popover>
```
**After (BUI Menu):**
```typescript
import {ButtonIcon, Menu, MenuItem, MenuTrigger} from '@backstage/ui';
import {RiMore2Line} from '@remixicon/react';
```tsx
import { ButtonIcon, Menu, MenuItem, MenuTrigger } from '@backstage/ui';
import { RiMore2Line } from '@remixicon/react';
<MenuTrigger>
<ButtonIcon aria - label = "more"
icon = { < RiMore2Line / >
}
variant = "secondary" / >
<Menu>
<MenuItem onAction = {handleAction} > Action < /MenuItem>
< /Menu>
< /MenuTrigger>;
<ButtonIcon aria-label="more" icon={<RiMore2Line />} variant="secondary" />
<Menu>
<MenuItem onAction={handleAction}>Action</MenuItem>
</Menu>
</MenuTrigger>;
```
### 12. List to BUI List
**Before (MUI List):**
```typescript
```tsx
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
<List>
@@ -570,7 +487,7 @@ import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
**After (BUI List):**
```typescript
```tsx
import { List, ListRow } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
@@ -587,7 +504,7 @@ Note: `ListRow` supports `icon`, `description`, `menuItems`, and `customActions`
**Before (MUI Chip):**
```typescript
```tsx
import { Chip } from '@material-ui/core';
<Chip label="Category" size="small" />;
@@ -595,17 +512,17 @@ import { Chip } from '@material-ui/core';
**After (BUI Tag):**
```typescript
import {Tag} from '@backstage/ui';
```tsx
import { Tag } from '@backstage/ui';
<Tag size = "small" > Category < /Tag>;
<Tag size="small">Category</Tag>;
```
### 14. Alert Pattern
**Before (MUI Alert):**
```typescript
```tsx
import { Alert, AlertTitle } from '@material-ui/lab';
<Alert severity="error">
@@ -616,7 +533,7 @@ import { Alert, AlertTitle } from '@material-ui/lab';
**After (BUI Alert):**
```typescript
```tsx
import { Alert } from '@backstage/ui';
<Alert
@@ -637,22 +554,21 @@ Use `loading` for a loading spinner, and `customActions` for action buttons.
**Before (MUI Icons):**
```typescript
```tsx
import CloseIcon from '@material-ui/icons/Close';
import SearchIcon from '@material-ui/icons/Search';
<CloseIcon / >
<SearchIcon fontSize = "small" / >
<CloseIcon />
<SearchIcon fontSize="small" />
```
**After (Remix Icons):**
```typescript
```tsx
import {RiCloseLine, RiSearchLine} from '@remixicon/react';
<RiCloseLine / >
<RiSearchLine size = {16}
/>
<RiCloseLine />
<RiSearchLine size={16} />
```
Common icon mappings:
@@ -683,6 +599,239 @@ Common icon mappings:
Find more icons at: https://remixicon.com/
### 16. Paper to Card
**Before (MUI Paper):**
```tsx
import { Paper, Typography } from '@material-ui/core';
<Paper elevation={2}>
<Typography variant="h6">Title</Typography>
<Typography>Body content</Typography>
</Paper>;
```
**After (BUI Card):**
```tsx
import { Card, CardHeader, CardBody, Text } from '@backstage/ui';
<Card>
<CardHeader>Title</CardHeader>
<CardBody>
<Text>Body content</Text>
</CardBody>
</Card>;
```
### 17. Select
**Before (MUI Select):**
```tsx
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
<FormControl fullWidth>
<InputLabel>Framework</InputLabel>
<Select value={value} onChange={e => setValue(e.target.value as string)}>
<MenuItem value="react">React</MenuItem>
<MenuItem value="angular">Angular</MenuItem>
</Select>
</FormControl>;
```
**After (BUI Select):**
```tsx
import { Select } from '@backstage/ui';
<Select
label="Framework"
selectedKey={value}
onSelectionChange={key => setValue(key as string)}
options={[
{ value: 'react', label: 'React' },
{ value: 'angular', label: 'Angular' },
]}
/>;
```
Note: BUI `Select` accepts flat `options` arrays or grouped `OptionSection` arrays. Pass `multiple` for multi-select.
### 18. Accordion
**Before (MUI Accordion):**
```tsx
import {
Accordion,
AccordionSummary,
AccordionDetails,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
Section title
</AccordionSummary>
<AccordionDetails>Content goes here</AccordionDetails>
</Accordion>;
```
**After (BUI Accordion):**
```tsx
import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
<Accordion>
<AccordionTrigger title="Section title" />
<AccordionPanel>Content goes here</AccordionPanel>
</Accordion>;
```
Use `AccordionGroup` to wrap multiple `Accordion` items and control whether multiple panels can be open simultaneously.
### 19. RadioGroup
**Before (MUI RadioGroup):**
```tsx
import {
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio,
} from '@material-ui/core';
<FormControl>
<FormLabel>Frequency</FormLabel>
<RadioGroup value={value} onChange={e => setValue(e.target.value)}>
<FormControlLabel value="daily" control={<Radio />} label="Daily" />
<FormControlLabel value="weekly" control={<Radio />} label="Weekly" />
</RadioGroup>
</FormControl>;
```
**After (BUI RadioGroup):**
```tsx
import { RadioGroup, Radio } from '@backstage/ui';
<RadioGroup label="Frequency" value={value} onChange={setValue}>
<Radio value="daily">Daily</Radio>
<Radio value="weekly">Weekly</Radio>
</RadioGroup>;
```
### 20. Badge
**Before (MUI Badge):**
```tsx
import { Badge } from '@material-ui/core';
<Badge badgeContent={4} color="primary">
<MailIcon />
</Badge>;
```
**After (BUI Badge):**
```tsx
import { Badge } from '@backstage/ui';
import { RiMailLine } from 'react-icons/ri';
<Badge>New</Badge>
<Badge size="small" icon={<RiMailLine size={12} />}>4</Badge>
```
Note: BUI `Badge` is a label-style badge (inline text with optional icon), not a notification counter overlay.
For notification counters overlaid on icons, use CSS positioning.
### 21. Slider
**Before (MUI Slider):**
```tsx
import { Slider } from '@material-ui/core';
<Slider
value={value}
onChange={(_, newValue) => setValue(newValue as number)}
min={0}
max={100}
step={10}
/>;
```
**After (BUI Slider):**
```tsx
import { Slider } from '@backstage/ui';
<Slider
label="Volume"
value={value}
onChange={setValue}
minValue={0}
maxValue={100}
step={10}
/>;
```
Note: BUI `Slider` `onChange` receives the new value directly. Use `minValue`/`maxValue` instead of `min`/`max`.
### 22. CheckboxGroup
**Before (MUI FormGroup with Checkboxes):**
```tsx
import {
FormControl,
FormLabel,
FormGroup,
FormControlLabel,
Checkbox,
} from '@material-ui/core';
<FormControl>
<FormLabel>Options</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={values.a}
onChange={e => handleChange('a', e.target.checked)}
/>
}
label="Option A"
/>
<FormControlLabel
control={
<Checkbox
checked={values.b}
onChange={e => handleChange('b', e.target.checked)}
/>
}
label="Option B"
/>
</FormGroup>
</FormControl>;
```
**After (BUI CheckboxGroup):**
```tsx
import { CheckboxGroup, Checkbox } from '@backstage/ui';
<CheckboxGroup label="Options" value={selected} onChange={setSelected}>
<Checkbox value="a">Option A</Checkbox>
<Checkbox value="b">Option B</Checkbox>
</CheckboxGroup>;
```
## CSS Variable Reference
### Spacing
@@ -733,8 +882,7 @@ Find more icons at: https://remixicon.com/
Some Backstage APIs still require MUI-compatible icon types:
- **NavItemBlueprint** (`@backstage/frontend-plugin-api`): The `icon` prop expects MUI `IconComponent` type. Remix icons
are not type-compatible.
- **PageBlueprint** (`@backstage/frontend-plugin-api`): The `icon` param on page extensions expects an `IconElement`. MUI icon components can still be used via `<Icon fontSize="inherit" />`.
- **Timeline** (`@material-ui/lab`): No BUI equivalent exists.
For these cases, keep using MUI components.
@@ -758,18 +906,23 @@ When migrating a plugin:
13. [ ] Replace MUI `Dialog` with BUI `DialogTrigger` pattern
14. [ ] Replace MUI `Tooltip` with BUI `TooltipTrigger` pattern (both from `@backstage/ui`)
15. [ ] Replace MUI `Tabs` with BUI `Tabs`
16. [ ] Replace MUI `Menu` with BUI `MenuTrigger` pattern
16. [ ] Replace MUI `Menu`/`Popover` with BUI `MenuTrigger` pattern
17. [ ] Replace `Chip` with `Tag`
18. [ ] Replace `IconButton` with `ButtonIcon`
19. [ ] Replace MUI `Alert` with BUI `Alert`
20. [ ] Replace MUI `List` with BUI `List` and `ListRow`
21. [ ] Update `Button` props (`disabled` → `isDisabled`, `variant="contained"` → `variant="primary"`)
22. [ ] Update `TextField` props (`required` → `isRequired`, `onChange` signature)
23. [ ] Replace MUI icons with Remix icons
24. [ ] Run `yarn tsc` to check for type errors
25. [ ] Run `yarn build` to verify build
26. [ ] Run `yarn lint` to check for missing dependencies
27. [ ] Test component rendering and functionality
21. [ ] Replace MUI `Select`/`FormControl` with BUI `Select`
22. [ ] Replace MUI `Accordion` with BUI `Accordion`/`AccordionTrigger`/`AccordionPanel`
23. [ ] Replace MUI `RadioGroup`/`FormControlLabel` with BUI `RadioGroup`/`Radio`
24. [ ] Replace MUI `FormGroup` with BUI `CheckboxGroup`
25. [ ] Replace MUI `Slider` with BUI `Slider`
26. [ ] Update `Button` props (`disabled` → `isDisabled`, `variant="contained"` → `variant="primary"`)
27. [ ] Update `TextField` props (`required` → `isRequired`, `onChange` signature)
28. [ ] Replace MUI icons with Remix icons
29. [ ] Run `yarn tsc` to check for type errors
30. [ ] Run the project's build command (e.g. `yarn build`, `yarn build:all`, or `yarn workspace <pkg> build`) to verify build
31. [ ] Run `yarn lint` to check for missing dependencies
32. [ ] Test component rendering and functionality
## Reference
@@ -0,0 +1,185 @@
---
name: plugin-analytics-instrumentation
description: Instrument a Backstage frontend plugin with analytics events using the Backstage Analytics API. Use this skill when adding, reviewing, or extending event capture (`captureEvent`, `AnalyticsContext`) in plugin components, deciding whether an interaction warrants an event, or writing tests for analytics behavior.
---
# Plugin Analytics Instrumentation Skill
This skill helps you add analytics instrumentation to a Backstage frontend plugin so that app integrators can measure how the plugin is used.
## Guiding principles
Follow these before writing a single `captureEvent` call.
### 1. Less is more — instrument semantic events, not every interaction
Capture events that represent things **your plugin is semantically responsible for** — the domain actions only your plugin knows how to describe. Events should reflect **user intent** (something a person chose to do), not the lifecycle of your UI. Avoid instrumenting generic UI noise that the framework or design system already handles.
Good candidates for plugin-owned events:
- A domain verb only your plugin performs (`deploy`, `create`, `merge`, `approve`, `trigger`, `refresh`, `rerun`).
- An outcome you uniquely know about (a search returning N results, a scaffolder template saving Y minutes, a task transitioning to a terminal state).
- A context-carrying interaction where the attributes matter (clicking a search result with its `rank` and `to` target).
Poor candidates — avoid these:
- Routine clicks on navigation links, buttons, tabs, menu items — these are covered by the `navigate` event and by built-in instrumentation in `@backstage/ui` (see next principle).
- Low-value UI state toggles (expanding a panel, opening a tooltip, hovering).
- Every field edit in a form — usually one `submit`-style event at the end captures the intent.
- Component lifecycle signals — mounts, unmounts, re-renders, effect firings, data fetches. These describe the machinery of the UI, not the user, and will fire in plenty of contexts the user never initiated (route prefetches, Suspense boundaries, tab switches). Narrow exceptions exist for terminal states the user _lands on_ (e.g. `not-found`).
- Events whose `action` and `subject` duplicate what is already captured upstream.
If you can't answer the question _"what question does this event help someone answer?"_ in one sentence, it's probably best not to add the event.
### 2. Prefer `@backstage/ui` components — they already instrument clicks
Components from `@backstage/ui` (BUI) have built-in click instrumentation wired to the Analytics API. As of today this includes at least `Link`, `ButtonLink`, `Tab`, `MenuItem`, `Tag`, and `Table` row clicks. When these components are used for navigation (i.e. rendered with an `href`), a `click` event is fired with the destination included as a `to` attribute. For most of them the `subject` is a best-effort human-readable label — the `aria-label`, the visible text, or the `href` as a fallback. `Table` rows are the exception: their `subject` is the `href` string itself, not derived from visible row content.
Consequences:
- If you render a `Link`/`ButtonLink` from `@backstage/ui`, you do **not** need to add a `click` event by hand. Doing so would produce duplicate events.
- If a plain `<a>` or a MUI button handles a navigation or action that you care about analytically, migrate it to the BUI equivalent first (see the `mui-to-bui-migration` skill). You'll get the click event for free and can focus your manual instrumentation on plugin-specific actions.
- Manual `captureEvent('click', ...)` calls are reserved for cases where **no** BUI component fits — for example, clicks on a canvas, a custom widget, or a non-link element whose interaction needs tracking.
#### Overriding the default event with `noTrack`
Occasionally a BUI component is the right UI primitive but the default event it fires isn't the one you want — for example, the interaction has a domain-specific verb (`approve`, `rerun`) rather than a generic `click`, or the subject should be a stable identifier rather than the visible link text. In that case, pass `noTrack` to suppress the built-in event and fire your own from the click handler:
```tsx
import { Link } from '@backstage/ui';
import { useAnalytics } from '@backstage/frontend-plugin-api';
function ApproveLink({ requestId, href }: Props) {
const analytics = useAnalytics();
return (
<Link
noTrack
href={href}
onClick={() => analytics.captureEvent('approve', requestId)}
>
Approve
</Link>
);
}
```
Reach for `noTrack` only when you're **replacing** the default event, not layering a second event on top of it. If both the default `click` and your custom event are useful, the custom one probably belongs on a different component or in a different handler. `noTrack` is available on all BUI components with built-in instrumentation (`Link`, `ButtonLink`, `Tab`, `MenuItem`, `Tag`, and `Table` rows).
### 3. Split events so analysis stays flexible
An `AnalyticsEvent` has an `action`, a `subject`, and surrounding `context` (which is filled in with `pluginId` and `extension` automatically). Keep each dimension disaggregated so questions can be answered at any level of granularity.
- **Action** is the verb — kept generic and reused across plugins (`click`, `search`, `filter`, `create`, `discover`). Avoid squashing what belongs in context into the action (e.g. don't use `filterEntityTable` — use `filter` and let the `extension` / `AnalyticsContext` identify the table).
- **Subject** is the noun — the specific thing acted upon (a PR name, a template name, a search term, a result title).
- **Attributes** are optional key/value dimensions available at capture time (`to`, `org`, `repo`, `entityRef`).
- **Context** is for metadata coming from further up the React tree, or shared across many events in a region.
When in doubt about attribute naming, reuse what existing events in the repo use (e.g. `entityRef` for catalog entities, `to` for destinations, `searchTypes` for search). Consistency across plugins makes aggregation possible.
## How to capture an event
Get a tracker with `useAnalytics()` and call `captureEvent(action, subject, options?)`.
```tsx
import { useAnalytics } from '@backstage/frontend-plugin-api';
function DeployButton({ serviceName }: { serviceName: string }) {
const analytics = useAnalytics();
const handleDeploy = () => {
// ...perform the deploy
analytics.captureEvent('deploy', serviceName);
};
return <Button onClick={handleDeploy}>Deploy</Button>;
}
```
For old-system plugins, the same hook is re-exported from `@backstage/core-plugin-api`; the behavior is identical. New plugins targeting the new frontend system should import from `@backstage/frontend-plugin-api`.
### Adding `value` and `attributes`
`value` is a single numeric metric associated with the event (duration, rank, count). `attributes` are dimensional string/number/boolean pairs.
```tsx
analytics.captureEvent('merge', pullRequestName, {
value: pullRequestAgeInMinutes,
attributes: { org, repo },
});
```
Keep attributes flat and serializable. Don't stuff large objects or PII in here.
### Using `AnalyticsContext` for ambient metadata
When the same attribute applies to many events under a subtree — or when the metadata lives further up the tree than the component firing the event — wrap the subtree in an `<AnalyticsContext>` instead of passing props down:
```tsx
import { AnalyticsContext } from '@backstage/frontend-plugin-api';
function TaskPage({ taskId, entityRef }: Props) {
return (
<AnalyticsContext attributes={{ taskId, entityRef }}>
<TaskToolbar />
<TaskTimeline />
</AnalyticsContext>
);
}
```
Every `captureEvent` fired inside that subtree will have `taskId` and `entityRef` merged into its `context`. Contexts nest and merge; inner values override outer ones.
Good uses of `AnalyticsContext`:
- Page- or route-level attributes that apply to every interaction on that page (`entityRef`, `taskId`, a tab selection).
- Cross-cutting aggregation keys that let app integrators group events (`segment`, `workspace`).
Don't wrap every small component in its own context — prefer to set context once at the boundary where the metadata first becomes available.
## Unit testing event capture
Use `mockApis.analytics()` from `@backstage/frontend-test-utils` — it returns a mock `AnalyticsApi` implementation with a `getEvents()` helper for assertions. Prefer one thorough test with multiple assertions over many small ones.
```tsx
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import { analyticsApiRef } from '@backstage/frontend-plugin-api';
import {
mockApis,
TestApiProvider,
wrapInTestApp,
} from '@backstage/frontend-test-utils';
it('captures a deploy event with the service name', async () => {
const analytics = mockApis.analytics();
render(
wrapInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<DeployButton serviceName="payments-api" />
</TestApiProvider>,
),
);
fireEvent.click(await screen.findByRole('button', { name: /deploy/i }));
await waitFor(() => {
expect(analytics.getEvents()[0]).toMatchObject({
action: 'deploy',
subject: 'payments-api',
});
});
});
```
Assert on `action`, `subject`, and any `attributes`/`value` you explicitly set. Don't assert on auto-populated context keys like `pluginId` — those are the framework's responsibility.
## Review checklist
Before submitting instrumentation changes:
1. [ ] Every new `captureEvent` call represents a **plugin-semantic, user-initiated** action (not a click already covered by BUI, a navigation, or a component-lifecycle trigger).
2. [ ] Route to a BUI component (`Link`, `ButtonLink`, `Tab`, `MenuItem`, `Tag`, `Table`) wherever one fits, rather than instrumenting a plain element by hand.
3. [ ] `action` is a short generic verb; plugin/extension identity is left to the auto-populated `context`.
4. [ ] Attribute keys reuse established conventions where applicable (`entityRef`, `to`, `searchTypes`, etc.).
5. [ ] Shared attributes are set via a single `<AnalyticsContext>` at a boundary, not duplicated across events.
6. [ ] `value` is numeric and meaningful (duration, rank, count) — not a stand-in for a string dimension.
7. [ ] No PII, secrets, tokens, or large serialized payloads in attributes.
8. [ ] At least one unit test covers each new event using `MockAnalyticsApi`.
+70 -12
View File
@@ -200,14 +200,21 @@ This can be enabled in the `auth-backend` plugin by using the `auth.experimental
auth:
experimentalClientIdMetadataDocuments:
enabled: true
# Optional: restrict which `client_id` URLs are allowed (defaults to ['*'])
allowedClientIdPatterns:
- 'https://example.com/*'
- 'https://*.trusted-domain.com/*'
# Optional: restrict which redirect URIs are allowed (defaults to ['*'])
allowedRedirectUriPatterns:
- 'http://localhost:*'
- 'https://*.example.com/*'
# Optional: override which client_id URLs are allowed.
# Defaults to Claude, VS Code, and the built-in Backstage CLI.
# Note: setting this replaces the defaults entirely. The built-in
# CLI pattern is derived from your auth backend's base URL and
# must be re-added manually if you override this list.
# allowedClientIdPatterns:
# - 'https://claude.ai/*'
# - 'https://vscode.dev/*'
# - 'https://my-custom-client.example.com/*'
# Optional: override which redirect URIs are allowed.
# Defaults to loopback addresses (localhost, 127.0.0.1, [::1]).
# allowedRedirectUriPatterns:
# - 'http://localhost:*'
# - 'http://127.0.0.1:*'
# - 'http://[::1]:*'
```
#### Dynamic Client Registration
@@ -224,10 +231,13 @@ This can be enabled in the `auth-backend` plugin by using the `auth.experimental
auth:
experimentalDynamicClientRegistration:
enabled: true
# Optional: limit valid callback URLs for added security
allowedRedirectUriPatterns:
- cursor://*
# Optional: restrict which redirect URIs are allowed.
# Defaults to Cursor and loopback addresses (localhost, 127.0.0.1, [::1]).
# allowedRedirectUriPatterns:
# - 'cursor://*'
# - 'http://localhost:*'
# - 'http://127.0.0.1:*'
# - 'http://[::1]:*'
```
## Configuring MCP Clients
@@ -292,3 +302,51 @@ The MCP Actions Backend emits metrics for the following operations:
- `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server
See the [OpenTelemetry tutorial](../tutorials/setup-opentelemetry.md) to learn how to make these metrics available.
## Tracing
The MCP Actions Backend emits a trace span for each `tools/call` invocation via the [Tracing Service](../backend-system/core-services/tracing.md), following the [OpenTelemetry server-side MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/#server). Each span uses the name `tools/call <toolname>`, server kind, and includes the standard MCP attributes (`mcp.method.name`, `gen_ai.tool.name`, `gen_ai.operation.name`). Known Backstage errors (such as `InputError` or `NotFoundError`) are caught and returned as `isError: true` tool responses — the span is marked with `error.type=tool_error` in that case. Unhandled exceptions are recorded automatically by the Tracing Service and the span status is set to `ERROR`.
In addition to those attributes, the Tracing Service automatically attaches the authenticated principal's type as `backstage.principal.type` (one of `user`, `service`, or `none`). Each `tools/call` span is also attributed to the plugin that owns the invoked action via `backstage.plugin.id` (e.g. `catalog`, `scaffolder`) — overriding the default `mcp-actions` value so tracing backends can filter activity by the source plugin rather than by the MCP transport.
### Baggage propagation
The MCP Actions routers propagate OpenTelemetry context from the incoming HTTP request headers so that trace parent and baggage survive through the MCP transport layer. The following low-cardinality identifier entries from the OpenTelemetry [`gen_ai.*` attribute registry](https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/), when set by the MCP client in baggage, are automatically forwarded as attributes on the `tools/call` span:
- `gen_ai.agent.id`
- `gen_ai.agent.name`
- `gen_ai.conversation.id`
- `gen_ai.provider.name`
- `gen_ai.request.model`
This enables tracing backends to correlate MCP tool invocations back to the originating agent, conversation, or model without additional configuration. Other `gen_ai.*` baggage entries are intentionally not forwarded — baggage may be set by arbitrary upstream callers, and a broad prefix filter would let clients smuggle high-cardinality or payload-shaped keys (e.g. `gen_ai.tool.call.result`, `gen_ai.prompt`) onto the span and bypass the [tool payload capture flag](#capturing-tool-arguments-and-results).
### Capturing the authenticated end user
The Tracing Service can additionally include the authenticated principal's identity as `enduser.id` (the user entity ref for a user principal, the service subject for a service principal). This is gated behind a backend-wide configuration flag and is **disabled by default**:
```yaml title="app-config.yaml"
backend:
tracing:
capture:
endUser: true # defaults to false
```
This flag is honored by every plugin that creates spans through the [Tracing Service](../backend-system/core-services/tracing.md), not just MCP Actions.
### Capturing tool arguments and results
When `mcpActions.tracing.capture.toolPayload` is enabled, the tool's input arguments and output result are recorded on the span as `gen_ai.tool.call.arguments` and `gen_ai.tool.call.result`.
```yaml title="app-config.yaml"
mcpActions:
tracing:
capture:
toolPayload: true # defaults to false
```
:::warning
These attributes are marked Opt-In by the OpenTelemetry GenAI semantic conventions because they may contain sensitive information — entity payloads, scaffolder inputs, free-form text, and so on. Only enable this flag if your tracing backend's data handling is appropriate for the kinds of payloads your MCP tools accept and produce.
:::
See the [OpenTelemetry tutorial](../tutorials/setup-opentelemetry.md) to learn how to make these spans available.
+4
View File
@@ -33,3 +33,7 @@ This is a (non-exhaustive) list of actions that are known to be part of the Acti
- `scaffolder.list-scaffolder-tasks` (List Scaffolder Tasks): This allows you to list scaffolder tasks that have been created.
- `scaffolder.execute-template` (Execute Scaffolder Template): Executes a Scaffolder template with its template ref and input parameter values.
- `scaffolder.get-scaffolder-task-logs` (Get Scaffolder Task Logs): This allows you to fetch the logs of a given scaffolder task.
### Search
- `search.query` (Query Search Engine): Query the Backstage search engine for documents across all or selected document types.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+1 -1
View File
@@ -496,6 +496,6 @@ This error can be caused by the following:
The second common error is: "Failed to sign-in, unable to resolve user identity". Here is what this looks like for the GitHub Auth provider:
![Failed to sign-in, unable to resolve user identity](../assets/auth/github-unable-to-reolve-identity.png)
![Failed to sign-in, unable to resolve user identity](../assets/auth/github-unable-to-resolve-identity.png)
This error is caused by the Sign-In Resolver you configured being unable to find a matching User in the Catalog. To fix this you need to import User, and Group, data from some source of truth for this data at your Organization. To do this you can use one of the existing Org Data providers like the ones for [Entra ID (Azure AD/MS Graph)](../integrations/azure/org.md), [GitHub](../integrations/github/org.md), [GitLab](../integrations/gitlab/org.md), etc. or if none of those fit your needs you can create a [Custom Entity Provider](../features/software-catalog/external-integrations.md#custom-entity-providers).
@@ -34,5 +34,6 @@ import { coreServices } from '@backstage/backend-plugin-api';
- [Root Logger Service](./root-logger.md) - Root-level logging.
- [Scheduler Service](./scheduler.md) - Scheduling of distributed background tasks.
- [Token Manager Service](./token-manager.md) - Deprecated service authentication service, use the [Auth Service](./auth.md) instead.
- [Tracing Service](./tracing.md) - Plugin-scoped trace span emission with built-in principal enrichment (alpha).
- [Url Reader Service](./url-reader.md) - Reading content from external systems.
- [User Info Service](./user-info.md) - Authenticated user information retrieval.
@@ -0,0 +1,211 @@
---
id: tracing
title: Tracing Service (alpha)
sidebar_label: Tracing Service (alpha)
description: Documentation for the Tracing service
---
The Tracing Service provides a unified interface for emitting application-level [OpenTelemetry](https://opentelemetry.io/) trace spans from Backstage backend plugins. It scopes each plugin's spans automatically using the OpenTelemetry [Instrumentation Scope](https://opentelemetry.io/docs/concepts/instrumentation-scope/), wraps span lifecycle (auto-end, exception recording, error status) so plugins don't need to write that boilerplate, and transparently enriches spans with the authenticated principal's identity when an HTTP request or `BackstageCredentials` is supplied.
:::note
This service is currently in **alpha** and is imported from `@backstage/backend-plugin-api/alpha`. The API may change in future releases.
:::
## Setting up OpenTelemetry
The Tracing Service does **not** configure the OpenTelemetry SDK itself. You are responsible for initializing the OpenTelemetry Node SDK — including exporters, samplers, and resource attributes — before starting the Backstage backend. Follow the [tutorial](../../tutorials/setup-opentelemetry.md) for more information.
## How it Relates to OpenTelemetry Auto-Instrumentation
The Tracing Service **complements** auto-instrumentation rather than replacing it. Auto-instrumentation captures infrastructure-level spans like inbound HTTP requests, outbound HTTP calls, and database queries automatically — including all the standard HTTP / DB attributes. The Tracing Service is for **application-level spans** that only your plugin can produce, and child spans you want to attach to that infrastructure work.
Because HTTP spans are auto-instrumented, you typically should **not** set `http.*` attributes on Tracing Service spans yourself — the parent HTTP span already carries them. Spans you create are children of that HTTP span, in the same trace.
## Using the Service
Since the Tracing Service is an alpha API, the service reference is imported from `@backstage/backend-plugin-api/alpha` instead of `coreServices`.
```ts
import { createBackendPlugin } from '@backstage/backend-plugin-api';
import { tracingServiceRef } from '@backstage/backend-plugin-api/alpha';
createBackendPlugin({
pluginId: 'todos',
register(env) {
env.registerInit({
deps: { tracing: tracingServiceRef },
async init({ tracing }) {
// ... wire up your routes/handlers, holding onto `tracing` ...
const result = await tracing.startActiveSpan(
'process-todo',
async span => {
span.setAttribute('todo.category', 'personal');
// ...do the work...
return computeResult();
},
);
},
});
},
});
```
`startActiveSpan(name, fn, options?)` runs `fn` inside a new active span. The span is finished automatically when `fn` resolves, and on a thrown error the exception is recorded, `error.type` is set from the error's `name`, and the span status is set to `ERROR` — you do not need to write a `try/catch/finally` for that.
Every span emitted through the service is automatically attributed to the calling plugin via `backstage.plugin.id` (matching `pluginMetadata.getId()`). Tracing backends can use this to filter all activity for a given plugin without inspecting the OpenTelemetry instrumentation scope. If your span represents work logically owned by a different plugin (for example, a wrapper that dispatches into another plugin's code), call `span.setAttribute('backstage.plugin.id', 'other-plugin')` from inside the callback to re-attribute it.
## Span Options
The third argument to `startActiveSpan` is an optional options object:
| Property | Type | Description |
| ------------- | -------------------------- | ---------------------------------------------------------------------------------------------------- |
| `attributes` | `TracingServiceAttributes` | Attributes to attach to the span at creation time. |
| `kind` | `TracingServiceSpanKind` | Span kind. Defaults to OpenTelemetry's `internal`. See [Span Kinds](#span-kinds). |
| `credentials` | `BackstageCredentials` | Authenticated principal source — adds principal-derived attributes to the span. |
| `request` | `Request` | HTTP request to extract credentials from (used only for principal extraction, not HTTP attribution). |
### Span Kinds
| Kind | Use Case |
| ------------ | --------------------------------------------------------------------------------- |
| `'internal'` | Default. Internal application work — e.g. processing pipelines, scheduled tasks. |
| `'server'` | Protocol-level inbound request handlers (e.g. an MCP `tools/call` server). |
| `'client'` | Outbound calls. Usually auto-instrumented at the HTTP / RPC client layer instead. |
| `'producer'` | Sending a message to a queue or stream. |
| `'consumer'` | Receiving a message from a queue or stream. |
Most Backstage application-level spans are `internal` — leave `kind` unset and OpenTelemetry's default applies.
## Setting Attributes and Status from Inside the Callback
The callback receives a span object on which you can set additional attributes or status:
```ts
await tracing.startActiveSpan('refresh-entity', async span => {
const entity = await fetchEntity(ref);
span.setAttribute('catalog.entity.kind', entity.kind);
if (entity.spec.deprecated) {
span.setStatus({ code: 'error', message: 'entity is deprecated' });
}
});
```
The span object exposes:
| Method | Description |
| ------------------------------ | -------------------------------------------------------------------- |
| `setAttribute(key, value)` | Set a single attribute. Value is a primitive or array of primitives. |
| `setStatus({ code, message })` | Set the span status. `code` is `'ok'`, `'error'`, or `'unset'`. |
## Context Propagation
The tracing service exposes two sub-objects that mirror the corresponding namespaces in `@opentelemetry/api`:
- `tracing.context` for context management (`active`, `with`).
- `tracing.propagation` for context propagation (`extract`, `getBaggage`, `getActiveBaggage`).
When your plugin handles a request through a transport or framework that doesn't automatically attach the caller's context to the work it runs (for example, a handler dispatched from a message-queue consumer, or a third-party transport like the MCP streamable HTTP transport that re-enters user code outside of Express's middleware chain), extract the trace parent and baggage from the inbound request's headers yourself and run the handler with that context active:
```ts
router.post('/', async (req, res) => {
const ctx = tracing.propagation.extract(
tracing.context.active(),
req.headers,
);
await tracing.context.with(ctx, () =>
transport.handleRequest(req, res, req.body),
);
});
```
`propagation.extract` reads from a header-shaped record (`Record<string, string | string[] | undefined>`), so any source of headers — Express's `req.headers`, a Node.js `http.IncomingMessage`, or a payload field carrying serialized headers — works the same way.
Any spans created inside the callback — including those from `startActiveSpan` — will be children of the propagated trace and will have access to the propagated baggage.
The context returned by `propagation.extract` and `context.active` is an opaque handle: consumers pass it back into the API but do not introspect it.
## Reading Baggage
Use `propagation.getActiveBaggage()` to read baggage entries from the currently active context. This is useful for forwarding caller-set metadata onto your spans — for example, a request ID, tenant identifier, or feature-flag context that the caller propagated via baggage. The baggage is exposed as a flat list of entries — iterate through them to find the keys you care about:
```ts
const baggage = tracing.propagation.getActiveBaggage();
for (const [key, entry] of baggage?.getAllEntries() ?? []) {
if (key === 'app.tenant.id') {
span.setAttribute('app.tenant.id', entry.value);
}
}
```
Use `propagation.getBaggage(ctx)` when you already hold a specific context handle (for example, one returned by `propagation.extract`) and want to read its baggage without first activating the context.
The returned object exposes:
| Method | Description |
| ----------------- | -------------------------------------------- |
| `getAllEntries()` | Returns all entries as `[key, { value }][]`. |
Both calls return `undefined` when no baggage is present. Single-key lookups are intentionally not provided — baggage is meant for bridging caller metadata onto spans or metrics, not as a general-purpose key-value store.
## Principal Enrichment
When you supply either `credentials` or a `request`, the service adds principal-derived attributes to the span:
- `backstage.principal.type` is always set when a principal is present (`'user'`, `'service'`, or `'none'`). This is a Backstage-specific extension.
- `enduser.id` is set **only when** [`backend.tracing.capture.endUser`](#capturing-the-authenticated-end-user) is enabled at the backend level. For a user principal this is the user entity ref (e.g. `user:default/alice`); for a service principal it is the service subject (e.g. `external:my-service`).
If both `credentials` and `request` are supplied, `credentials` wins — the service does not extract from the request. The `request` is used only for credential extraction and does not influence other span attributes.
```ts
async ({ credentials }) => {
await tracing.startActiveSpan(
'process-tool-call',
async span => {
// ... span automatically has backstage.principal.type, and (if enabled)
// enduser.id matching the credentials' principal ...
},
{ credentials },
);
};
```
### Capturing the authenticated end user
The `backend.tracing.capture.endUser` flag controls whether Tracing Service spans include the authenticated principal's identity as `enduser.id`. It defaults to `false` so identity is not exported by default.
```yaml title="app-config.yaml"
backend:
tracing:
capture:
endUser: true # defaults to false
```
This is a backend-wide configuration honored by every plugin that creates spans through this service.
## Per-Plugin Tracer Configuration
Each plugin automatically receives a tracer named `backstage-plugin-<pluginId>`. Operators can override the OpenTelemetry Instrumentation Scope for a specific plugin without code changes:
```yaml title="app-config.yaml"
backend:
tracing:
plugin:
catalog:
tracer:
name: 'custom-catalog-tracer'
version: '2.0.0'
schemaUrl: 'https://example.com/schema'
```
| Property | Type | Default | Description |
| ----------- | -------- | ----------------------------- | -------------------------------- |
| `name` | `string` | `backstage-plugin-<pluginId>` | Name of the OpenTelemetry tracer |
| `version` | `string` | — | Version string for the tracer |
| `schemaUrl` | `string` | — | Schema URL for the tracer |
:::tip
Most plugins won't need any of this — the defaults are designed to attribute every plugin's spans uniquely without configuration.
:::
+10 -2
View File
@@ -228,6 +228,14 @@ helps downstream localization.
- Use (`-`) for unordered lists.
- Leave a blank line after each list.
- Indent nested lists with two spaces.
- Use a numbered list for a sequence of steps rather than prose with
"First", "Then", and "Finally". Numbered lists are easier to scan, make
the order explicit, and give readers a clear way to reference a specific
step.
| Do | Don't |
| :----------------------------------------------------------------- | :------------------------------------------------------------------------------ |
| 1) Install the package. 2) Run the migration. 3) Start the server. | First, install the package. Then, run the migration. Finally, start the server. |
### Tables
@@ -353,7 +361,7 @@ A list of Backstage-specific terms and words to be used consistently across
the site.
| Term | Usage |
| :----------------- | :------------------------------------------------------------------------------------------------------------------------------------- | --- |
| :----------------- | :------------------------------------------------------------------------------------------------------------------------------------- |
| Backstage | Always capitalized. |
| plugin | Lowercase when referring to the concept. Use code style when referring to a specific package, for example `@backstage/plugin-catalog`. |
| Software Catalog | Capitalized as a product name. Use "catalog" (lowercase) when referring to the concept generically. |
@@ -362,4 +370,4 @@ the site.
| Scaffolder | Capitalized as a product name. |
| app-config | Use code style: `app-config.yaml`. |
| open source | Two words, lowercase (unless starting a sentence). |
| backend system | Lowercase when referring to the Backstage backend framework. | |
| backend system | Lowercase when referring to the Backstage backend framework. |
+1 -1
View File
@@ -165,7 +165,7 @@ are separated out into their own folder, see further down.
- [`techdocs-node/`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) -
Common node.js functionalities for TechDocs, to be shared between
[techdocs-backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend)
plugin and [techdocs-cli](https://github.com/backstage/techdocs-cli).
plugin and [techdocs-cli](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli).
- [`test-utils/`](https://github.com/backstage/backstage/tree/master/packages/test-utils) -
This package contains general purpose testing facilities for testing a
@@ -52,16 +52,15 @@ _Example disabling the search page extension_
app:
extensions:
- page:search: false # ✨
- nav-item:search: false # ✨
```
_Example setting the search sidebar item title_
_Example setting the search page title (used in the sidebar)_
```yaml
# app-config.yaml
app:
extensions:
- nav-item:search: # ✨
- page:search: # ✨
config:
title: 'Search Page'
```
@@ -109,6 +109,10 @@ Default secrets are resolved from environment variables and accessible via `${{
## Customizing the ScaffolderPage with Grouping and Filtering
The sections below cover the legacy (JSX) frontend system. For the new
frontend system, see [Customizing the templates page in the new frontend system](#customizing-the-templates-page-in-the-new-frontend-system)
below.
Once you have more than a few software templates you may want to customize your
`ScaffolderPage` by grouping and surfacing certain templates together. You can
accomplish this by creating `groups` and passing them to your `ScaffolderPage`
@@ -149,3 +153,74 @@ You can have several use cases for that:
}
/>
```
## Customizing the templates page in the new frontend system
In the new frontend system the templates page is built from extensions, so
customisations are configured rather than passed as JSX props.
### Defining template groups in `app-config.yaml`
The `sub-page:scaffolder/templates` extension accepts a `groups` config field.
Each group has a `title` and a `filter` predicate (using
[entity predicate queries](https://backstage.io/docs/features/software-catalog/catalog-customization#entity-predicate-queries)).
Templates not matched by any group fall into an automatically appended
"Other Templates" group. With no groups configured the page renders a single
"Templates" group.
```yaml
app:
extensions:
- sub-page:scaffolder/templates:
config:
groups:
- title: Recommended Services
filter:
spec.type: service
- title: Documentation
filter:
spec.type: documentation
```
Predicate values are matched case-insensitively. The matchers `$exists`,
`$in`, `$contains`, `$hasPrefix` and the logical operators `$all`, `$any`, `$not`
are also supported — see the
[entity predicate queries reference](https://backstage.io/docs/features/software-catalog/catalog-customization#entity-predicate-queries)
for the full grammar.
### Replacing the default `TemplateCard`
The `TemplateCard` exported from `@backstage/plugin-scaffolder-react/alpha`
is a swappable component. Apps can replace it by registering a
`SwappableComponentBlueprint` extension that targets `TemplateCard`:
```tsx
// packages/app/src/modules/appModuleScaffolder.tsx
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import { TemplateCard } from '@backstage/plugin-scaffolder-react/alpha';
export const appModuleScaffolder = createFrontendModule({
pluginId: 'app',
extensions: [
SwappableComponentBlueprint.make({
name: 'scaffolder-template-card',
params: defineParams =>
defineParams({
component: TemplateCard,
loader: () => import('./MyTemplateCard').then(m => m.MyTemplateCard),
}),
}),
],
});
```
Wire the module into your app by adding `appModuleScaffolder` to the
`features` array of `createApp` in `packages/app/src/App.tsx`.
`MyTemplateCard` receives `TemplateCardComponentProps`
(`{ template, additionalLinks?, onSelected? }`). The list takes care of
binding the template to `onSelected`, so the card just calls
`props.onSelected?.()` to choose itself. The example app under
`packages/app/src/modules/BuiTemplateCard.tsx` shows a Backstage UI (BUI)
implementation you can use as a starting point.
@@ -73,34 +73,20 @@ You don't need to provide any extra configuration, but you have to be sure that
## Form Decorators
Form decorators provide the ability to run arbitrary code before the form is submitted along with secrets to the `scaffolder-backend` plugin. They are provided to the `app` using a Utility API.
Form decorators provide the ability to run arbitrary code before the form is submitted along with secrets to the `scaffolder-backend` plugin.
#### Installation
#### Configuring templates
To install the Form Decorators, add the following to your `packages/app/src/apis.ts`:
```ts
createApiFactory({
api: formDecoratorsApiRef,
deps: {},
factory: () =>
DefaultScaffolderFormDecoratorsApi.create({
decorators: [
// add decorators here
],
}),
}),
```
And then you'll also need to define which decorators run in each template using the `EXPERIMENTAL_formDecorators` key in the template's `spec`:
Define which decorators run in each template using the `formDecorators` key in the template's `spec`:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: my-template
spec:
EXPERIMENTAL_formDecorators:
- id: myDecorator
formDecorators:
- id: mockDecorator
input:
test: something funky
@@ -108,36 +94,67 @@ spec:
steps: ...
```
#### Creating a Decorator
:::note
The legacy `EXPERIMENTAL_formDecorators` field is still supported but deprecated. Migrate to `formDecorators` when possible.
:::
You can create a decorator using the simple helper method `createScaffolderFormDecorator`:
#### Creating a decorator
Create a decorator with `createScaffolderFormDecorator` and register it as an extension using `FormDecoratorBlueprint`:
```ts
export const mockDecorator = createScaffolderFormDecorator({
// give the decorator a name
id: 'mockDecorator',
import { createScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
import { githubAuthApiRef } from '@backstage/core-plugin-api';
// define the schema for the input that can be provided in `template.yaml`
const mockDecorator = createScaffolderFormDecorator({
id: 'mockDecorator',
schema: {
input: {
test: z => z.string(),
},
},
deps: {
// define dependencies here
githubApi: githubAuthApiRef,
},
decorator: async (
// Context has all the things needed to write simple decorators
{ setSecrets, setFormState, input: { test } },
// Depepdencies injected here
{ githubApi },
) => {
// mutate the form state
setFormState(state => ({ ...state, test, mock: 'MOCK' }));
// mutate the form secrets
setSecrets(state => ({ ...state, GITHUB_TOKEN: 'MOCK_TOKEN' }));
const token = await githubApi.getAccessToken(['repo']);
setFormState(state => ({ ...state, test }));
setSecrets(state => ({ ...state, GITHUB_TOKEN: token }));
},
});
```
#### Installation (new frontend system)
Register your decorator as an extension using `FormDecoratorBlueprint`:
```ts
import { FormDecoratorBlueprint } from '@backstage/plugin-scaffolder-react/alpha';
export const myDecoratorExtension = FormDecoratorBlueprint.make({
name: 'my-decorator',
params: {
decorator: mockDecorator,
},
});
```
Then install the extension in your app or plugin.
#### Installation (legacy frontend system)
For apps using the legacy frontend system, provide decorators through a Utility API in `packages/app/src/apis.ts`:
```ts
createApiFactory({
api: formDecoratorsApiRef,
deps: {},
factory: () =>
DefaultScaffolderFormDecoratorsApi.create({
decorators: [mockDecorator],
}),
}),
```
@@ -746,6 +746,75 @@ input:
When `each` is used, the outputs of a repeated step are returned as an array of outputs from each iteration.
### Status Check Functions - `always()` and `failure()`
By default, when a step fails during a scaffolder run, all subsequent steps are skipped and the task is marked as failed. This can be problematic when your template creates external resources (repositories, cloud infrastructure, deployments) that need to be cleaned up if a later step fails.
Status check functions give you control over which steps run even after a failure. You use them inside a `${{ ... }}` template expression in the `if` field of a step.
| Function | Description |
| ----------- | ---------------------------------------------------------------------------- |
| `always()` | Always runs the step, regardless of whether previous steps passed or failed. |
| `failure()` | Runs the step only when a previous step has failed. |
These functions must be used as template expressions such as `${{ always() }}` or `${{ failure() }}`.
After a step has failed, the scaffolder only attempts later steps whose `if` expression invokes one of these status check functions.
#### Usage
```yaml
steps:
- id: cleanup
name: Cleanup Resources
action: my:cleanup:action
if: ${{ always() }}
```
#### Example: Cleanup on failure
A common pattern is to create resources in early steps and add cleanup steps
that only run if something goes wrong:
```yaml
steps:
- id: create-repo
name: Create Repository
action: publish:github
input:
repoUrl: ${{ parameters.repoUrl }}
- id: deploy
name: Deploy to Kubernetes
action: deploy:kubernetes
input:
manifest: ./k8s/deployment.yaml
# Only runs when a previous step failed — cleans up the repository
- id: cleanup-repo
name: Delete Repository
action: github:repo:delete
if: ${{ failure() }}
input:
repoUrl: ${{ parameters.repoUrl }}
# Always runs — post an audit event regardless of outcome
- id: audit
name: Post Audit Event
action: debug:log
if: ${{ always() }}
input:
message: 'Scaffolder run completed for ${{ parameters.repoUrl }}'
# Does not run after a failure, because it does not invoke a status check function
- id: plain-truthy-condition
name: Plain Truthy Condition
action: debug:log
if: ${{ true }}
input:
message: 'This step is skipped after a previous failure'
```
## Outputs
Each individual step can output some variables that can be used in the
+1 -1
View File
@@ -31,7 +31,7 @@ The static files consist of HTML, CSS and Images generated by MkDocs. We remove
all the JavaScript before adding them to Backstage for security reasons. And
there is an additional `techdocs_metadata.json` file that TechDocs needs to
render a site. It's important that you use either
[techdocs-cli](https://github.com/backstage/techdocs-cli) or
[techdocs-cli](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli) or
[techdocs-container](https://github.com/backstage/techdocs-container) to
generate the docs for the expected output.
+2
View File
@@ -149,6 +149,8 @@ Options:
Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file.
--legacyCopyReadmeMdToIndexMd Attempt to ensure an index.md exists falling back to using <docs-dir>/README.md or README.md
in case a default <docs-dir>/index.md is not provided. (default: false)
--disableExternalFonts Disable external font downloads for all TechDocs sites. Useful for air-gapped environments
where Google fonts cannot be accessed. (default: false)
--runAsDefaultUser Bypass setting the container user as the same user and group id as host for Linux and MacOS (default: false)
-v, --verbose Enable verbose output. (default: false)
-h, --help display help for command
+1 -1
View File
@@ -87,7 +87,7 @@ documentation for publishing. Currently it mostly acts as a wrapper around the
TechDocs container and provides an easy-to-use interface for our docker
container.
[TechDocs CLI](https://github.com/backstage/techdocs-cli)
[TechDocs CLI](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli)
## TechDocs Reader
+27
View File
@@ -97,6 +97,33 @@ techdocs:
legacyCopyReadmeMdToIndexMd: false
```
#### Disable external fonts
`techdocs.generator.mkdocs.disableExternalFonts`
(Optional) Use this when the generator cannot reach the internet (for example air-gapped or restricted networks). MkDocs Material otherwise tries to download the Roboto font from Google during generation.
When `true`, TechDocs patches each `mkdocs.yml` during generation: if no `theme` section exists it adds `name: material` and `font: false`; if a `theme` exists but `font` is omitted, it sets `font: false`; if `font` is already set in the file, your value is left unchanged.
**Example:**
```yaml
techdocs:
generator:
mkdocs:
disableExternalFonts: true
```
Alternatively, configure `mkdocs.yml` manually:
```yaml
theme:
name: material
font: false
```
**Note:** When using `theme.font` in `mkdocs.yml`, `theme.name: material` is required. If `font` is already set in the file, app-config patching does not override it; it only adds `font: false` when `font` was not configured.
#### Default Plugins
`techdocs.generator.mkdocs.defaultPlugins`
+1 -1
View File
@@ -92,7 +92,7 @@ the source code hosting provider. Notice that instead of the `dir:` prefix, the
`url:` prefix is used instead. For example:
- **GitHub**: `url:https://githubhost.com/org/repo/tree/<branch_name>`
- **GitLab**: `url:https://gitlabhost.com/org/repo/tree/<branch_name>`
- **GitLab**: `url:https://gitlabhost.com/org/repo`
- **Bitbucket**: `url:https://bitbuckethost.com/project/repo/src/<branch_name>`
- **Azure**: `url:https://azurehost.com/organization/project/_git/repository`
+30 -7
View File
@@ -93,7 +93,7 @@ the source code hosting provider. Notice that instead of the `dir:` prefix, the
`url:` prefix is used instead. For example:
- **GitHub**: `url:https://githubhost.com/org/repo/tree/<branch_name>`
- **GitLab**: `url:https://gitlabhost.com/org/repo/tree/<branch_name>`
- **GitLab**: `url:https://gitlabhost.com/org/repo`
- **Bitbucket**: `url:https://bitbuckethost.com/project/repo/src/<branch_name>`
- **Azure**: `url:https://azurehost.com/organization/project/_git/repository`
@@ -373,9 +373,24 @@ on how you have configured your `template.yaml`.
Done! You now have support for TechDocs in your own software template!
### Prevent download of Google fonts
### Disable external fonts
If your Backstage instance does not have internet access, the generation will fail. TechDocs tries to download the Roboto font from Google. You can disable it by adding the following lines to mkdocs.yaml:
`techdocs.generator.mkdocs.disableExternalFonts`
(Optional) Use this when the generator cannot reach the internet (for example air-gapped or restricted networks). MkDocs Material otherwise tries to download the Roboto font from Google during generation.
When `true`, TechDocs patches each `mkdocs.yml` during generation: if no `theme` section exists it adds `name: material` and `font: false`; if a `theme` exists but `font` is omitted, it sets `font: false`; if `font` is already set in the file, your value is left unchanged.
**Example:**
```yaml
techdocs:
generator:
mkdocs:
disableExternalFonts: true
```
Alternatively, configure `mkdocs.yml` manually:
```yaml
theme:
@@ -383,11 +398,19 @@ theme:
font: false
```
:::note Note
**Note:** When using `theme.font` in `mkdocs.yml`, `theme.name: material` is required. If `font` is already set in the file, app-config patching does not override it; it only adds `font: false` when `font` was not configured.
The addition `name: material` is necessary. Otherwise it will not work
#### Using techdocs-cli in CI/CD
:::
When generating TechDocs sites in CI/CD workflows using `techdocs-cli`, you can
use the `--disableExternalFonts` flag:
```bash
techdocs-cli generate --disableExternalFonts
```
This will automatically patch the `mkdocs.yml` file during the generation
process, just like the `app-config.yaml` option does for local generation.
## How to enable iframes in TechDocs
@@ -805,7 +828,7 @@ metadata:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-platfrom
name: example-platform
title: Example Application Platform
namespace: default
description: This is the child entity
@@ -207,7 +207,7 @@ If you need to migrate documentation objects from an older-style path
format including case-sensitive entity metadata, you will need to add some
additional permissions to be able to perform the migration, including:
- `s3:PutBucketAcl` (for copying files,
- `s3:PutObjectAcl` (for copying files,
[more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html))
- `s3:DeleteObject` and `s3:DeleteObjectVersion` (for deleting migrated files,
[more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html))
@@ -129,26 +129,20 @@ A plugin might not always behave exactly the way you want. It could be that you
```tsx
import plugin from '@backstage/plugin-catalog';
import { PageBlueprint } from '@backstage/frontend-plugin-api';
import { RiLayoutGridLine } from '@remixicon/react';
export default plugin.withOverrides({
// These overrides are merged with the original extensions
extensions: [
// Override the catalog nav item to use a custom icon
plugin.getExtension('nav-item:catalog').override({
factory: origFactory => [
NavItemBlueprint.dataRefs.target({
...origFactory().get(NavItemBlueprint.dataRefs.target),
icon: CustomCatalogIcon,
// Override the catalog index page with a custom icon and implementation
plugin.getExtension('page:catalog').override({
factory: origFactory =>
origFactory({
icon: <RiLayoutGridLine />,
loader: () =>
import('./CustomCatalogIndexPage').then(m => <m.Page />),
}),
],
}),
// Override the catalog index page with a completely custom implementation
PageBlueprint.make({
params: {
path: '/catalog',
routeRef: plugin.routes.catalogIndex,
loader: () => import('./CustomCatalogIndexPage').then(m => <m.Page />),
},
}),
],
});
@@ -164,7 +164,6 @@ Extension responsible for rendering the logo and items in the app's sidebar.
| Name | Description | Type | Optional | Default | Extension creator |
| ------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
| content | Overrides the default content of the navbar. | [NavContentBlueprint.dataRefs.component](https://backstage.io/api/stable/variables/_backstage_plugin-app-react.NavContentBlueprint.html) | true | - | [NavContentBlueprint](https://backstage.io/api/stable/variables/_backstage_plugin-app-react.NavContentBlueprint.html) |
| items | Nav items target objects. | [createNavItemExtension.targetDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension.targetdataref) | true | - | [createNavItemExtension](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) |
### App routes
@@ -686,7 +686,7 @@ createApp({
#### App Root Sidebar
New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items. Nav items are auto-discovered from page extensions registered under `app/routes` (no explicit `NavItemBlueprint` required), with metadata from page config, nav item extensions, or plugin defaults.
New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items. Nav items are auto-discovered from page extensions registered under `app/routes`, with metadata from page config or plugin defaults.
In order to migrate your existing sidebar, you will want to create an override for the `app/nav` extension. You can do this by copying the standard of having a `src/modules/nav/` folder, which can contain an extension which you can install into the `app` in the form of a `module`.
@@ -740,14 +740,7 @@ The deprecated `items` prop (a flat list compatible with `<SidebarItem {...item}
You might also notice that when you're rendering additional fixed icons for plugins (e.g. Search in a dedicated group) these might become duplicated, since that page is also included in `nav.rest()`. To exclude an item from the remaining list, call `nav.take('page:search')` before calling `nav.rest()` — you can discard the return value. Items that have been taken will not appear in `rest()`.
You can also use the old `NavItemBlueprint`-based nav item extensions to disable items from the nav bar, these can be disabled in config without affecting the page itself:
```yaml title="in app-config.yaml"
app:
extensions:
- nav-item:search: false
- nav-item:catalog: false
```
To hide a page from the sidebar without disabling the page itself, use `nav.take('page:...')` in your custom sidebar implementation before calling `nav.rest()`, or disable the page extension in config with `page:<plugin-id>: false`.
#### App Root Routes
@@ -44,9 +44,9 @@ The plugin ID should be a lowercase dash-separated string, while the plugin inst
## Adding extensions
The plugin that we created above is empty, and doesn't provide any actual functionality. To add functionality to a plugin you need to create and provide it with one or more [extensions](../architecture/20-extensions.md). Let's continue by adding a standalone page to our plugin, as well as a navigation item that allows users to navigate to the page.
The plugin that we created above is empty, and doesn't provide any actual functionality. To add functionality to a plugin you need to create and provide it with one or more [extensions](../architecture/20-extensions.md). Let's continue by adding a standalone page to our plugin, with a title and icon that appear in the app sidebar.
To create a new extension you typically use pre-defined [extension blueprints](../architecture/23-extension-blueprints.md), provided either by the framework itself or by other plugins. In this case we'll use `PageBlueprint` and `NavItemBlueprint`, both from `@backstage/frontend-plugin-api`. We will also need to [create a route reference](../architecture/36-routes.md#creating-a-route-reference) to use as a reference for our page, allowing us to dynamically create URLs that link to our page.
To create a new extension you typically use pre-defined [extension blueprints](../architecture/23-extension-blueprints.md), provided either by the framework itself or by other plugins. In this case we'll use `PageBlueprint` from `@backstage/frontend-plugin-api`. We will also need to [create a route reference](../architecture/36-routes.md#creating-a-route-reference) to use as a reference for our page, allowing us to dynamically create URLs that link to our page.
```tsx title="in src/routes.ts"
import { createRouteRef } from '@backstage/frontend-plugin-api';
@@ -62,8 +62,8 @@ export const rootRouteRef = createRouteRef();
import {
createFrontendPlugin,
PageBlueprint,
NavItemBlueprint,
} from '@backstage/frontend-plugin-api';
import { RiPuzzleLine } from '@remixicon/react';
import { rootRouteRef } from './routes';
// Note that these extensions aren't exported, only the plugin itself is.
@@ -75,6 +75,10 @@ const examplePage = PageBlueprint.make({
// This is the default path of this page, but integrators are free to override it
path: '/example',
// The title and icon are used to populate the app sidebar automatically
title: 'Example',
icon: <RiPuzzleLine />,
// Page extensions are always dynamically loaded using React.lazy().
// All of the functionality of this page is implemented in the
// ExamplePage component, which is a regular React component.
@@ -84,19 +88,10 @@ const examplePage = PageBlueprint.make({
},
});
// This nav item is provided to the app.nav extension, and will by default be rendered as a sidebar item
const exampleNavItem = NavItemBlueprint.make({
params: {
routeRef: rootRouteRef,
title: 'Example',
icon: ExampleIcon, // Custom SvgIcon, or one from the Material UI icon library
},
});
// The same plugin as above, now with the extensions added
export const examplePlugin = createFrontendPlugin({
pluginId: 'example',
extensions: [examplePage, exampleNavItem],
extensions: [examplePage],
// We can also make routes available to other plugins.
// highlight-start
routes: {
@@ -106,7 +101,7 @@ export const examplePlugin = createFrontendPlugin({
});
```
What we've built here is a very common type of plugin. It's a top-level tool that provides a single page, along with a method for navigating to that page. The implementation of the page component, in this case the highlighted `ExamplePage`, can be arbitrarily complex. It can be anything from a single simple information page, to a full-blown application with multiple sub-pages.
What we've built here is a very common type of plugin. It's a top-level tool that provides a single page, which the app discovers and links to from the sidebar automatically. The implementation of the page component, in this case the highlighted `ExamplePage`, can be arbitrarily complex. It can be anything from a single simple information page, to a full-blown application with multiple sub-pages.
We have also provided external access to our route reference by passing it to the plugin `routes` option. This makes it possible for app integrators to bind an external link from a different plugin to our plugin page. You can read more about how this works in the [External Route References](../architecture/36-routes.md#external-route-references) section.
@@ -182,7 +177,7 @@ const exampleApi = ApiBlueprint.make({
});
// highlight-add-end
/* Omitted definitions for examplePage, exampleNavItem, and rootRouteRef. */
/* Omitted definitions for examplePage and rootRouteRef. */
export const examplePlugin = createFrontendPlugin({
pluginId: 'example',
@@ -190,7 +185,6 @@ export const examplePlugin = createFrontendPlugin({
// highlight-add-next-line
exampleApi,
examplePage,
exampleNavItem,
],
routes: {
root: rootRouteRef,
@@ -227,7 +221,6 @@ export const examplePlugin = createFrontendPlugin({
exampleEntityContent,
exampleApi,
examplePage,
exampleNavItem,
],
routes: {
root: rootRouteRef,
@@ -15,10 +15,6 @@ These are the [extension blueprints](../architecture/23-extension-blueprints.md)
An API extension is used to add or override [Utility API factories](../utility-apis/01-index.md) in the app. They are commonly used by plugins for both internal and shared APIs. There are also many built-in Api extensions provided by the framework that you are able to override.
### NavItem (deprecated) - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.index.NavItemBlueprint.html)
The `NavItemBlueprint` is deprecated. The app now auto-discovers navigation items from page extensions, so explicit nav item extensions are no longer needed. To migrate, ensure your plugin and/or page extensions have a `title` and `icon` set — these are used to populate the sidebar automatically.
### Page - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.index.PageBlueprint.html)
Page extensions provide content for a particular route in the app. By default pages are attached to the app routes extensions, which renders the root routes. Pages automatically inherit the plugin's `title` and `icon` as defaults, which can be overridden per-page via `PageBlueprint` params.
@@ -8,11 +8,11 @@ description: Configuring or overriding Swappable Components
# Swappable components
Swappable components are a feature of the frontend system that allow you to replace the implementations of components that are used in your Backstage app.
These Swappable Components are defined using `createSwappableComponent` and then can be exported from a plugins `-react` package in order to be used in both other plugins, and to be rebound to a new implementation by the Backstage Integrator.
These Swappable Components are defined using `createSwappableComponent` and then can be exported from a plugin's `-react` package in order to be used in both other plugins, and to be rebound to a new implementation by the Backstage Integrator.
## Creating a Swappable Component
In order to create a Swappable Component, you need to use the `createSwappableComponent` function from the `@backstage/frontend-plugin-api` package. You can supply a default implementation for the component, as well as a way to separate both the props of the external component and in the implementation of the component.
In order to create a Swappable Component, you need to use the `createSwappableComponent` function from the `@backstage/frontend-plugin-api` package. You can supply a default implementation for the component, as well as a way to separate the props of the external component from the props used by the implementation of the component.
```tsx title="in @internal/plugin-example-react"
import { createSwappableComponent } from '@backstage/frontend-plugin-api';
@@ -20,9 +20,9 @@ import { createSwappableComponent } from '@backstage/frontend-plugin-api';
export const ExampleSwappableComponent = createSwappableComponent({
name: 'example',
// This is a loader for loading the default implementation of the component when there's no overriden
// This is a loader for loading the default implementation of the component when there's no overriding
// implementation created with `SwappableComponentBlueprint`.
// It can be sync like below, but is can also be async like `loader: () => import('./DefaultImplementation').then(m => m.DefaultImplementation)`.
// It can be sync like below, but it can also be async like `loader: () => import('./DefaultImplementation').then(m => m.DefaultImplementation)`.
loader: () => (props: { name: string }) =>
<div>Your name is {props.name}</div>,
@@ -60,7 +60,7 @@ import appPlugin from '@backstage/plugin-app';
const app = createApp({
features: [
// Using a module to provide the extenion to the app
// Using a module to provide the extension to the app
createFrontendModule({
pluginId: 'app',
extensions: [
@@ -74,7 +74,7 @@ const app = createApp({
}),
],
}),
// Core components that already ship with the app plugin can be overriden using getExtension()
// Core components that already ship with the app plugin can be overridden using getExtension()
appPlugin.withOverrides({
extensions: [
appPlugin.getExtension('component:app/core-progress').override({
@@ -94,9 +94,9 @@ const app = createApp({
Currently there are only three different built-in Swappable Components that you can replace the implementations of, and these live in `@backstage/frontend-plugin-api`. They are as follows:
- `<Progress />
- `<ErrorDisplay />
- `<NotFoundErrorPage />
- `<Progress />`
- `<ErrorDisplay />`
- `<NotFoundErrorPage />`
You can see more about these components at their [definition](https://github.com/backstage/backstage/blob/master/packages/frontend-plugin-api/src/components/DefaultSwappableComponents.tsx), and their default implementations are shipped inside the [`app-plugin`](https://github.com/backstage/backstage/blob/master/plugins/app/src/extensions/components.tsx).
@@ -177,6 +177,16 @@ const analytics = useAnalytics();
analytics.captureEvent('deploy', serviceName);
```
The events you capture should reflect user intent and domain actions your
plugin is uniquely responsible for, rather than generic clicks or UI
lifecycle events. Many `@backstage/ui` components (such as `Link`,
`ButtonLink`, `Tab`, `MenuItem`, `Tag`, and `Table` rows) often capture
`click` events automatically, so you rarely need to instrument
navigation-style clicks by hand. If one of those components is the right UI
primitive but the default event is not what you want to capture, pass the
`noTrack` prop to suppress it and call `captureEvent` from your own click
handler instead.
### Providing Extra Attributes
Additional dimensional `attributes` as well as a numeric `value` can be provided
+87
View File
@@ -0,0 +1,87 @@
---
id: docker
sidebar_label: 001 - Building the Docker image
title: Building the Docker image
description: How to build your Backstage app into a deployable Docker image
---
Audience: Developers and Admins
## Summary
Every production deployment of Backstage starts with a Docker image. The image
bundles both the frontend and backend into a single artifact that you can deploy
anywhere containers run.
By the end of this page, you will have a Docker image ready to push to a
container registry.
## What is in the Docker image?
When you created your app with `@backstage/create-app`, a `Dockerfile` was
generated at `packages/backend/Dockerfile`. The build process layers both the
frontend and backend into a single image:
1. The **backend** is compiled and bundled into `packages/backend/dist/`.
2. The **frontend** is built and served by the
`@backstage/plugin-app-backend` plugin, which is included in the backend
by default.
3. Production dependencies are installed, and the result is packaged into a
slim Node.js image.
The image runs as a non-root `node` user and sets `NODE_ENV=production`.
## Building the image
The recommended approach is a **host build**, where compilation happens on
your machine (or CI runner) and Docker only packages the result. This is faster
and produces better caching behavior.
From the root of your repository:
```shell
yarn install
yarn tsc
yarn build:backend
```
Then build the Docker image:
```shell
docker image build . -f packages/backend/Dockerfile --tag backstage
```
To verify it works locally:
```shell
docker run -it -p 7007:7007 backstage
```
You should see logs in your terminal and be able to open `http://localhost:7007`
in your browser.
:::tip Troubleshooting
If you run into build issues, two Docker flags can help:
- `--progress=plain` shows verbose output instead of folded log sections.
- `--no-cache` rebuilds all layers from scratch.
```shell
docker image build . -f packages/backend/Dockerfile --tag backstage --progress=plain --no-cache
```
:::
## Further reading
For a full multi-stage Docker build (where everything happens inside Docker)
or for deploying the frontend separately, see the
[Building a Docker image](../../deployment/docker.md) reference documentation.
## Next steps
Before deploying, you need to set up two production dependencies: a database and
an authentication provider.
- [Setting up a production database](./002-database.md)
+103
View File
@@ -0,0 +1,103 @@
---
id: database
sidebar_label: 002 - Database
title: Setting up a production database
description: How to configure PostgreSQL for your Backstage production deployment
---
Audience: Admins
## Summary
During local development, Backstage uses SQLite as a fast in-memory database.
SQLite does not persist data across restarts and is not designed for
multi-instance deployments, so you need a dedicated database for production.
By the end of this page, you will have PostgreSQL configured as your Backstage
database.
## Why PostgreSQL?
PostgreSQL is the recommended production database for Backstage. It handles
concurrent connections well, supports the query patterns that Backstage plugins
use, and is available as a managed service from every major cloud provider.
Some options for running PostgreSQL:
- **Managed services**: Amazon RDS, Google Cloud SQL, Azure Database for
PostgreSQL, or similar offerings.
- **Self-hosted**: Running PostgreSQL in a container or on a dedicated server.
- **In Kubernetes**: Deploying PostgreSQL alongside Backstage (useful for
getting started, but managed services are preferred for production).
## Configuring Backstage to use PostgreSQL
Open your `app-config.production.yaml` and add the database configuration:
```yaml title="app-config.production.yaml"
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
```
The `${...}` syntax references environment variables, which keeps secrets out
of your configuration files. Set these variables in your deployment environment.
:::caution
Avoid hardcoding database credentials in configuration files. Use environment
variables or a secrets manager provided by your deployment platform.
:::
### Optional: SSL connections
If your database provider requires SSL (most managed services do), add the
SSL configuration:
```yaml title="app-config.production.yaml"
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
ssl:
require: true
rejectUnauthorized: true
```
If your provider uses a custom certificate authority, you can reference the
CA certificate file:
```yaml
ssl:
ca:
$file: /path/to/ca/server.crt
```
## How Backstage uses the database
Each plugin gets its own isolated database schema. Migrations run automatically
when the backend starts, so you do not need to run any manual migration steps.
The database coordinates state and work distribution between instances, which
is what makes horizontal scaling possible later on.
## Further reading
For step-by-step PostgreSQL installation instructions and cloud-specific
setup guides, see the [Database configuration guide](../../getting-started/config/database.md).
## Next steps
With the database in place, the next step is to replace the guest login with
a real authentication provider.
- [Configuring authentication](./003-authentication.md)
@@ -0,0 +1,90 @@
---
id: authentication
sidebar_label: 003 - Authentication
title: Configuring authentication
description: How to set up a production authentication provider for Backstage
---
Audience: Developers and Admins
## Summary
By default, Backstage uses a guest authentication provider that lets anyone
log in without credentials. This is convenient for local development but
creates a security risk when deployed to production. Before deploying, you
should configure a real authentication provider.
By the end of this page, you will understand what needs to change and where
to find the setup instructions for your chosen provider.
## Why replace guest authentication?
The guest provider is explicitly not intended for containerized or production
environments. With guest auth enabled, any user who can reach your Backstage
instance shares the same identity and has the same level of access. Replacing
it is one of the most important steps before going to production.
:::danger
Deploying Backstage with the guest authentication provider exposes your
instance to unauthorized access. Configure a real provider before deploying.
:::
## Choosing an authentication provider
Backstage supports a wide range of authentication providers. GitHub is a
common choice since most developers already have accounts, but you should
pick whatever your organization already uses for single sign-on.
Some popular options:
| Provider | Good fit when... |
| :------------------- | :------------------------------------------------ |
| GitHub | Your team uses GitHub and you want a quick setup. |
| Microsoft / Azure AD | Your company uses Microsoft Entra ID (Azure AD). |
| Google | Your company uses Google Workspace. |
| Okta | You use Okta as your identity provider. |
| OIDC | You have a generic OpenID Connect provider. |
| OAuth2 Proxy | You already use an authenticating reverse proxy. |
The full list of providers and their configuration is available in the
[Authentication documentation](../../auth/index.md).
## What the setup involves
Regardless of which provider you choose, the setup follows the same pattern:
1. **Create an OAuth application** (or equivalent) with your identity
provider. You will get a client ID and client secret.
2. **Add the provider configuration** to `app-config.yaml` with the
credentials, typically using environment variables for secrets.
3. **Install the backend auth module** for your chosen provider in
`packages/backend`.
4. **Configure a sign-in resolver** that maps the authenticated user identity
to a Backstage catalog user entity.
5. **Update the frontend** to show the correct sign-in page.
The [Authentication getting started guide](../../getting-started/config/authentication.md)
walks through this process step by step using GitHub as the example provider.
## Production configuration
Once authentication is configured, make sure that the guest provider is
disabled in your production config:
```yaml title="app-config.production.yaml"
auth:
providers:
guest: null
```
This ensures that even if the guest provider is configured in your base
`app-config.yaml` for local development, it is explicitly disabled in
production.
## Next steps
With both a database and authentication configured, you are ready to deploy.
- [Deploying to production](./004-deploying.md)
@@ -0,0 +1,110 @@
---
id: deploying
sidebar_label: 004 - Deploying
title: Deploying to production
description: How to deploy your Backstage instance to production
---
Audience: Admins
## Summary
You have a Docker image, a database, and authentication configured. Now it
is time to deploy. The _best_ way to deploy Backstage is _the same way_ you
deploy other software at your organization. Backstage is designed to run as
a stateless Node.js application backed by an external PostgreSQL database,
so it fits into most existing deployment pipelines without special tooling.
This page describes what every Backstage deployment needs, regardless of
platform, and points to the reference guides for specific targets.
## What every deployment needs
Whichever platform you choose, the deployment will need to take care of the
following concerns:
- **A container image** built from your repository and pushed to a registry
your runtime can pull from. The image you built in
[Building the Docker image](./001-docker.md) is the artifact that gets
deployed.
- **Configuration and secrets** delivered to the running container as
environment variables or mounted files. This includes database
credentials, auth provider client secrets, and any integration tokens.
- **A reachable PostgreSQL database** that the running instance can connect
to using the credentials from the previous step. See
[Configuring the database](./002-database.md) for details.
- **A network entry point** — typically an ingress, load balancer, or
reverse proxy — that exposes the backend on port `7007` to your users
over HTTPS.
- **A health-checked runtime** that can restart the container if it stops
responding and roll out new versions when you publish a new image.
- **`app.baseUrl` and `backend.baseUrl`** in your
`app-config.production.yaml` set to the public URL where users will
access Backstage. Auth providers and the frontend rely on these matching
the actual entry point:
```yaml title="app-config.production.yaml"
app:
baseUrl: https://backstage.example.com
backend:
baseUrl: https://backstage.example.com
listen:
port: 7007
```
## Choosing a deployment target
Backstage runs anywhere a Node.js container can run. Pick the option that
matches what your organization already operates — you do not need to adopt
new infrastructure to run Backstage.
| Target | Good fit when... |
| :----------------------- | :---------------------------------------------------------------------- |
| Kubernetes | Your organization already runs services on Kubernetes. |
| Amazon ECS / Fargate | You are on AWS and prefer managed container scheduling. |
| Google Cloud Run | You want a fully managed, request-driven container runtime on GCP. |
| Azure Container Apps | You are on Azure and want a managed container platform. |
| A traditional VM or PaaS | You prefer running the Node.js process directly behind a reverse proxy. |
| Docker Compose | You are running a small installation or proof of concept. |
Backstage maintains a reference guide for the Kubernetes path in
[Deploying with Kubernetes](../../deployment/k8s.md), which walks through
namespaces, secrets, the deployment, the service, and connecting to
PostgreSQL inside the cluster.
For other platforms, the
[community-contributed deployment guides](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/)
in the Backstage repository cover targets such as ECS, Cloud Run, and
Docker Compose, and the [deployment overview](../../deployment/index.md)
explains the underlying model that all of these guides share.
## Operational concerns
A few operational details apply to every deployment and are worth getting
right before opening Backstage up to your users:
- **Run multiple replicas** behind your load balancer. Backstage is
stateless, so multiple instances can serve traffic against the same
PostgreSQL database. We cover this further in
[Scaling Backstage](./007-scaling.md).
- **Store secrets securely.** Container platforms typically offer a
secrets primitive — Kubernetes Secrets, AWS Secrets Manager, GCP Secret
Manager, Azure Key Vault, and so on. Reference these from your
configuration with environment variables rather than committing
credentials.
- **Enable health checks.** Wire your platform's readiness and liveness
probes to the Backstage health endpoints so unhealthy instances are
taken out of rotation and restarted automatically.
- **Run behind HTTPS.** Terminate TLS at your ingress, load balancer, or
reverse proxy and make sure the public URL is what `app.baseUrl` and
`backend.baseUrl` are set to.
If you need to run Backstage behind a corporate proxy, see the
[corporate proxy guide](../../tutorials/corporate-proxy.md).
## Next steps
Your Backstage instance is deployed. Next, let's look at how to manage
configuration effectively across environments.
- [Config-first development](./005-config-first.md)
@@ -0,0 +1,92 @@
---
id: config-first
sidebar_label: 005 - Configuration management
title: Config-first development
description: Managing Backstage configuration across environments
---
Audience: Developers and Admins
## Summary
One of the things that makes Backstage easier to operate over time is treating
configuration as the primary way to control application behavior. Instead of
writing custom code for every change, many behaviors can be toggled, adjusted,
or extended through configuration files.
By the end of this page, you will understand how Backstage configuration
layering works and how to manage it across environments.
## How configuration layering works
Backstage loads configuration from multiple `app-config*.yaml` files and
merges them together. Files loaded later override values from earlier files.
The Docker image built in the [first step](./001-docker.md) starts with this
command:
```
node packages/backend --config app-config.yaml --config app-config.production.yaml
```
This means `app-config.production.yaml` overrides any values set in
`app-config.yaml`. You can use this pattern to keep your base config for
local development and override only what changes in production.
### Common configuration split
| File | Purpose |
| :--------------------------- | :------------------------------------------------- |
| `app-config.yaml` | Base configuration shared across all environments. |
| `app-config.local.yaml` | Local overrides, not committed to source control. |
| `app-config.production.yaml` | Production-specific overrides. |
## Environment variables in config
Use the `${VAR_NAME}` syntax to reference environment variables. This is
the recommended approach for secrets and values that differ between
environments:
```yaml title="app-config.production.yaml"
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
```
This keeps secrets out of your repository and lets the same Docker image be
used across environments by changing the variables at deploy time.
## What to configure vs. what to code
A good rule of thumb: if a change affects _where_ something connects, _how_
it authenticates, or _which_ features are enabled, it belongs in configuration.
If it changes _what_ a feature does, it belongs in code.
Examples of config-driven behavior:
- Database connection details.
- Authentication provider selection and credentials.
- Catalog locations and entity providers.
- Integration tokens (GitHub, GitLab, etc.).
- TechDocs storage backend (local, S3, GCS, Azure).
- Proxy endpoints for external services.
Making configuration your primary lever for environment differences simplifies
your CI/CD pipeline. You build one Docker image and deploy it everywhere,
varying only the config files and environment variables.
## Further reading
For the full configuration reference, see the
[Configuration documentation](../../conf/index.md).
## Next steps
With your deployment running and configuration managed, you should set up
monitoring to keep it healthy.
- [Monitoring your deployment](./006-monitoring.md)
@@ -0,0 +1,85 @@
---
id: monitoring
sidebar_label: 006 - Monitoring
title: Monitoring your deployment
description: Setting up OpenTelemetry and frontend analytics for your Backstage deployment
---
Audience: Admins
## Summary
A production Backstage deployment needs monitoring to track health, diagnose
issues, and understand usage patterns. Backstage provides built-in support
for OpenTelemetry on the backend and an Analytics API on the frontend.
By the end of this page, you will know how to set up both.
## Backend monitoring with OpenTelemetry
Backstage uses [OpenTelemetry](https://opentelemetry.io/) to report metrics
and traces. The setup involves installing a few OpenTelemetry packages,
creating an instrumentation file, and loading it before the backend starts.
Follow the [Setup OpenTelemetry tutorial](../../tutorials/setup-opentelemetry.md)
for step-by-step instructions on installing dependencies, configuring
exporters (Prometheus for metrics, OTLP for traces), and wiring everything
into your backend.
### Key metrics to monitor
Backstage plugins emit metrics that give you insight into system health.
Common examples include:
- `catalog_entities_count` - Total number of entities in the catalog.
- `catalog.processed.entities.count` - Number of entities processed.
- `catalog.processing.duration` - Time spent processing entities.
- `scaffolder.task.count` - Number of scaffolder tasks run.
- `scaffolder.task.duration` - Time taken by scaffolder tasks.
The specific metric names may vary depending on which plugins you have
installed and their versions. These examples help you set up alerts for
things like processing backlogs or unusually slow scaffolder runs.
### Health checks
Backstage provides built-in health check endpoints that you can use for
liveness and readiness probes in Kubernetes or other orchestrators:
- `/.backstage/health/v1/readiness` - Returns healthy when the backend is
ready to serve traffic.
- `/.backstage/health/v1/liveness` - Returns healthy when the backend
process is alive.
## Frontend analytics
Backstage provides an Analytics API for tracking user behavior on the
frontend. This is useful for understanding which plugins get the most use
and measuring the return on your Backstage investment.
Several analytics tools are supported through community plugins:
- Google Analytics 4
- New Relic Browser
- Matomo
See the [Analytics documentation](../../frontend-system/building-plugins/08-analytics.md)
for setup instructions.
For frontend error reporting, consider integrating with a service like
Sentry, CloudWatch RUM, or Cloudflare RUM to catch and diagnose client-side
errors.
## Logging
The backend emits structured JSON logs on stdout by default. These logs
include fields like `service`, `plugin`, `level`, and `message` that make
them easy to parse with log aggregation tools (Elasticsearch, Datadog,
Splunk, etc.).
## Next steps
As more users adopt your Backstage instance, you may need to scale the
deployment.
- [Scaling your deployment](./007-scaling.md)
@@ -0,0 +1,96 @@
---
id: scaling
sidebar_label: 007 - Scaling
title: Scaling your deployment
description: How to scale Backstage as usage grows
---
Audience: Admins
## Summary
A single Backstage instance handles many users well, but as your organization
grows and more plugins are added, you may need to scale. This page covers the
strategies available.
## Horizontal scaling
The most straightforward approach is to run multiple identical instances of
Backstage behind a load balancer. All instances share the same external
database (and optional cache or search services). The backend plugins
coordinate through the database to share state and distribute work.
In Kubernetes, this is as simple as increasing the replica count in your
deployment:
```yaml
spec:
replicas: 3
```
No additional configuration is needed. The database handles coordination
between instances.
## Splitting the backend
For larger installations, you can break the backend into multiple services,
each running a different set of plugins. For example, you might run the
catalog and scaffolder as separate deployments so that heavy catalog
processing does not affect scaffolder performance.
This is a more advanced approach that requires:
- Separate backend packages, each importing only the plugins they need.
- A custom `DiscoveryService` implementation that routes requests to the
correct backend based on the plugin ID.
- Routing both external (ingress) and internal (backend-to-backend) traffic
appropriately.
See the
[backend system documentation](../../backend-system/building-backends/01-index.md#split-into-multiple-backends)
for details on how to set this up.
## Separating the frontend
By default, the frontend is served from your backend deployment using the
`@backstage/plugin-app-backend` plugin. If you need to reduce load on the
backend or serve the frontend from a CDN for better performance, you can
deploy the frontend separately.
This involves:
1. Removing the `@backstage/plugin-app-backend` plugin from the backend.
2. Building the frontend as a static bundle.
3. Serving it from a separate container (for example, NGINX) or a static
hosting provider.
An example NGINX setup is available in the
[contrib/docker/frontend-with-nginx](https://github.com/backstage/backstage/blob/master/contrib/docker/frontend-with-nginx)
folder.
:::note
When serving the frontend separately, configuration is no longer injected by
the backend at runtime. You need to provide the correct configuration at
frontend build time.
:::
## When to scale
Here are some signals that indicate you should consider scaling:
- API response times are increasing.
- Catalog processing is falling behind (visible in the
`catalog.processing.duration` metric).
- Scaffolder tasks are queuing for longer than expected.
- Users report slow page loads.
Start with horizontal scaling (more replicas) before considering backend
splitting. It is simpler and handles most growth scenarios.
## Further reading
For more details on scaling strategies, see the
[Scaling Backstage Deployments](../../deployment/scaling.md) reference
documentation.
+38
View File
@@ -0,0 +1,38 @@
---
id: index
title: Deploying Backstage to production
description: A guided path for deploying your Backstage app to production
---
## Prerequisites
- You have completed the [create-app golden path](../create-app/index.md) and
have a working Backstage app.
- Your code is pushed to a source control management system (GitHub, GitLab,
etc.).
- You have a general understanding of how your company builds and deploys
software.
## What should I get out of this guide?
This guide walks through everything you need to get your Backstage instance
running in a production environment. By the end, you will have:
- A Docker image containing your Backstage app.
- A production database (PostgreSQL) connected to your deployment.
- A real authentication provider replacing the default guest login.
- A running deployment, whether on Kubernetes, ECS, or another platform.
- Monitoring and observability set up with OpenTelemetry.
- An understanding of how to scale your deployment as usage grows.
## Structure
We start with the Docker image since that is the foundation of any deployment.
Then we set up the two critical pre-deploy dependencies: a database and
authentication. After that, we walk through deploying to Kubernetes and discuss
other deployment options. Finally, we cover operational topics like
configuration management, monitoring, and scaling.
## Next steps
- [Building the Docker image](./001-docker.md)
@@ -13,8 +13,323 @@ You may have noticed that your list of TODOs disappears after you restart your B
SQLite is the default database for local development. It runs in memory (and can also run from a file on disk). It supports quick iteration cycles and can be easily deleted if anything goes wrong.
### What does our data look like at rest?
Writing to a database requires a table, which requires us to chat quickly about what we want to store. Our TODO object with `title`, `id`, `createdBy` and `createdAt` keys is a good fit to map 1:1 with our database schema.
## Adding the `databaseService` to your plugin
<!--TODO-->
### The plumbing
To start, let's just plumb through the general `databaseService` usage we expect.
First, add a new service dependency on `databaseService`,
```diff file="src/services/TodoListService.ts"
export const todoListServiceRef = createServiceRef<Expand<TodoListService>>({
id: 'todo.list',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {
logger: coreServices.logger,
catalog: catalogServiceRef,
+ database: coreServices.database,
},
async factory(deps) {
return TodoListService.create(deps);
},
}),
});
```
We then need to add it to our service,
```diff file="src/services/TodoListService.ts"
+import type { Knex } from 'knex';
import {
coreServices,
createServiceFactory,
createServiceRef,
LoggerService,
+ DatabaseService,
} from '@backstage/backend-plugin-api';
export class TodoListService {
+ readonly #database: Knex;
- readonly #storedTodos = new Array<TodoItem>();
- static create(options: {
+ static async create(options: {
logger: LoggerService;
catalog: typeof catalogServiceRef.T;
+ database: DatabaseService;
}) {
const knex = await options.database.getClient();
- return new TodoListService(options.logger, options.catalog);
+ return new TodoListService(options.logger, options.catalog, knex);
}
private constructor(
logger: LoggerService,
catalog: typeof catalogServiceRef.T,
+ database: Knex,
) {
this.#logger = logger;
this.#catalog = catalog;
+ this.#database = database;
}
```
And with that, we have an isolated `knex` client to communicate with our database!
### Creating your table
Unfortunately, without tables in our database, our `knex` client is not doing much. We need to create a _migration_. Knex stores migrations as JavaScript/TypeScript files that get executed as part of a call to `knex.migrate.latest()`. By default, these are stored in a `migrations/` directory.
Let's get started. First, we need to install `knex` as a dependency so both its CLI and imported `Knex` types are available,
```bash
yarn workspace @internal/plugin-todo-backend add knex
```
Now, running this command will scaffold a file in that `migrations/` directory for us.
```bash
yarn workspace @internal/plugin-todo-backend knex migrate:make init --migrations-directory ./migrations
```
This should spit out a message like
```bash
Created Migration: ~/Projects/backstage/backstage/plugins/todo-backend/migrations/20260323130057_init.js
```
Let's open that file,
```js
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function up(knex) {
// await knex.schema...
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function down(knex) {
// await knex.schema...
};
```
You can see two functions, `up` and `down`. `up` is called to apply a migration and `down` is used to undo a previous migration. These should be reversible - if you call `up` and then `down` the database should generally be in the same state if those commands hadn't been run.
Let's create our table,
```diff
exports.up = async function up(knex) {
+ await knex.schema.createTable('todo', table => {
+ table.uuid('id').primary();
+ table.string('created_by', 255).notNullable();
+ table.string('title').notNullable();
+ table.datetime('created_at').defaultTo(knex.fn.now()).notNullable();
+ table.index(['created_by'], 'todo_user_idx');
});
};
```
You'll notice that we use `snake_case` instead of `camelCase` - that's how SQL is conventionally written.
Let's make sure that we don't forget to add a `down` migration as well!
```diff
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
+ await knex.schema.dropTable('todo');
};
```
Now, we need to actually tell our `knex` client to automatically apply these migrations. We'll add the `database` service to our plugin's `init` function,
```diff file="src/plugin.ts"
import {
coreServices,
createBackendPlugin,
+ resolvePackagePath,
} from '@backstage/backend-plugin-api';
// ...
deps: {
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
+ logger: coreServices.logger,
+ database: coreServices.database,
todoList: todoListServiceRef,
},
- async init({ httpAuth, httpRouter, todoList }) {
+ async init({ httpAuth, logger, httpRouter, database, todoList }) {
+ const knex = await database.getClient();
+
+ if (!database.migrations?.skip) {
+ logger.info('Running database migrations...');
+
+ const migrationsDir = resolvePackagePath(
+ '@internal/plugin-todo-backend',
+ 'migrations',
+ );
+
+ await knex.migrate.latest({
+ directory: migrationsDir,
+ });
+ }
httpRouter.use(
await createRouter({
httpAuth,
todoList,
}),
);
```
Walking through what we've written -
1. `database.migrations?.skip` - convention for migrations to allow them to be skipped through config.
1. `const migrationsDir = resolvePackagePath` - ensure the correct migrations directory is passed regardless of environment.
1. `await knex.migrate.latest(` - actually run the migration, calls our `up` method we wrote above.
We also need to do 1 more thing,
```diff file="package.json"
"files": [
- "dist"
+ "dist",
+ "migrations"
],
```
This will make sure the migrations in our plugin work for all users.
For those who want more details, the full [Knex migration docs](https://knexjs.org/guide/migrations.html#migration-cli) are very informative!
### Defining our types
Now that we have our table, we need to add types for it to protect against runtime incompatibilities. For now, these are hand written.
```diff title="src/services/TodoListService.ts"
+export interface TodoDatabaseRow {
+ title: string;
+ id: string;
+ created_by: string;
+ created_at: string;
+}
export interface TodoItem {
title: string;
id: string;
createdBy: string;
createdAt: string;
}
```
Notice the change to snake case as it has to match the database schema we have above. Now we need to transform `TodoItem` to `TodoDatabaseRow` for writes and `TodoDatabaseRow` to `TodoItem` for reads.
```diff title="src/services/TodoListService.ts"
private constructor(
logger: LoggerService,
catalog: typeof catalogServiceRef.T,
+ database: Knex,
) {
this.#logger = logger;
this.#catalog = catalog;
+ this.#database = database;
}
+ private toDatabaseRow(todo: TodoItem): TodoDatabaseRow {
+ return {
+ id: todo.id,
+ title: todo.title,
+ created_by: todo.createdBy,
+ created_at: todo.createdAt,
+ };
+ }
+ private fromDatabaseRow(row: TodoDatabaseRow): TodoItem {
+ return {
+ id: row.id,
+ title: row.title,
+ createdBy: row.created_by,
+ createdAt: row.created_at,
+ };
+ }
```
And that's it! You're now set up to actually read from and write to your database.
### Writing to your table
Creating your table was a solid chunk of work - thankfully, writing to it is going to be much easier!
```diff title="src/services/TodoListService.ts"
async createTodo(
// ...
const id = crypto.randomUUID();
const createdBy = options.credentials.principal.userEntityRef;
const newTodo = {
title,
id,
createdBy,
createdAt: new Date().toISOString(),
};
- this.#storedTodos.push(newTodo);
+ await this.#database
+ .insert(this.toDatabaseRow(newTodo))
+ .into('todo');
return newTodo;
}
```
We've basically just updated our service call to use `this.#database` instead of `this.#storedTodos`.
### Reading from your table
Now that we have things in our database, how do we actually get them back out again?
```diff title="src/services/TodoListService.ts"
async listTodos(): Promise<{ items: TodoItem[] }> {
- return { items: Array.from(this.#storedTodos) };
+ const rows = await this.#database('todo').select();
+ return { items: rows.map(row => this.fromDatabaseRow(row)) };
}
async getTodo(request: { id: string }): Promise<TodoItem> {
- const todo = this.#storedTodos.find(item => item.id === request.id);
+ const item = await this.#database('todo').where({ id: request.id }).first();
- if (!todo) {
+ if (!item) {
throw new NotFoundError(`No todo found with id '${request.id}'`);
}
- return todo;
+ return this.fromDatabaseRow(item);
}
```
And we're done!
## Testing your changes
To validate this flow, let's use the same commands that we ran in [the last section of this guide](./002-poking-around.md#testing-locally).
If everything is working correctly, you will see the same response that you did last time.
@@ -1,5 +1,5 @@
---
id: source-tracked
id: reading-from-source
sidebar_label: 004 - Integrating with SCMs
title: 004 - Git-tracked TODOs
description: How to ingest TODOs from source code repositories into your plugin
+2
View File
@@ -31,6 +31,7 @@ catalog:
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
accountId: '123456789012' # optional, uses the main account otherwise
schedule: # same options as in SchedulerServiceTaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
@@ -51,6 +52,7 @@ catalog:
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
accountId: '123456789012' # optional, uses the main account otherwise
schedule: # same options as in SchedulerServiceTaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
+37 -4
View File
@@ -26,7 +26,7 @@ catalog:
default:
tenantId: ${AZURE_TENANT_ID}
user:
filter: accountEnabled eq true and userType eq 'member'
filter: userType eq 'member'
group:
filter: >
securityEnabled eq false
@@ -50,6 +50,37 @@ backend.add(import('@backstage/plugin-catalog-backend-module-msgraph'));
/* highlight-add-end */
```
## Incremental Ingestion for Large Tenants
For very large Azure AD tenants where loading the full dataset into memory at once is not feasible, the `@backstage/plugin-catalog-backend-module-msgraph-incremental` package provides a memory-efficient alternative. It processes users and groups one page at a time and persists the `@odata.nextLink` cursor so ingestion resumes from the last completed page after a pod restart.
```bash title="From your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-incremental-ingestion
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph-incremental
```
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend'));
/* highlight-add-start */
backend.add(
import('@backstage/plugin-catalog-backend-module-incremental-ingestion'),
);
backend.add(
import('@backstage/plugin-catalog-backend-module-msgraph-incremental'),
);
/* highlight-add-end */
```
It uses the same `catalog.providers.microsoftGraphOrg` configuration as the standard module. The following options are **not** supported by the incremental provider: `userGroupMember*` and `groupIncludeSubGroups`. Use `MicrosoftGraphOrgEntityProvider` if you require those.
| | `MicrosoftGraphOrgEntityProvider` | Incremental provider |
| -------------------------- | --------------------------------- | -------------------- |
| Memory usage | Full dataset in RAM | One page at a time |
| Resume on restart | Starts from scratch | Resumes from cursor |
| `userGroupMember*` options | Supported | Not supported |
| `groupIncludeSubGroups` | Supported | Not supported |
| Suitable for large tenants | No | Yes |
## Authenticating with Microsoft Graph
### Local Development
@@ -90,8 +121,9 @@ To grant the managed identity the same permissions as mentioned in _App Registra
## Filtering imported Users and Groups
By default, the plugin will import all users and groups from your directory.
This can be customized through [filters](https://learn.microsoft.com/en-us/graph/filter-query-parameter) and [search](https://learn.microsoft.com/en-us/graph/search-query-parameter) queries. Keep in mind that if you omit filters and search queries for the user or group properties, the plugin will automatically import all available users or groups.
By default, the plugin will import all **enabled** users and all groups from your directory.
Disabled user accounts (`accountEnabled eq false`) are automatically excluded.
This can be further customized through [filters](https://learn.microsoft.com/en-us/graph/filter-query-parameter) and [search](https://learn.microsoft.com/en-us/graph/search-query-parameter) queries. Any custom `user.filter` is combined with the base `accountEnabled eq true` filter using `and`.
### Groups
@@ -125,12 +157,13 @@ By default the provider will get groups using the msgraph `/group` endpoint, but
### Users
There are two modes for importing users - You can import all user objects matching a `filter`.
The `accountEnabled eq true` base filter is applied automatically and combined with any custom filter you provide.
```yaml
microsoftGraphOrg:
providerId:
user:
filter: accountEnabled eq true and userType eq 'member'
filter: userType eq 'member'
```
Alternatively you can import users that are members of specific groups.
@@ -62,9 +62,9 @@ In case after a proper configuration, the events still are not being captured: C
service: 'backstage',
env: '<%= config.getString("app.datadogRum.env") %>',
sampleRate:
'<%= config.getOptionalNumber("app.datadogRum.sessionSampleRate") || 100 %>',
<%= config.getOptionalNumber("app.datadogRum.sessionSampleRate") || 100 %>,
sessionReplaySampleRate:
'<%= config.getOptionalNumber("app.datadogRum.sessionReplaySampleRate") || 0 %>',
<%= config.getOptionalNumber("app.datadogRum.sessionReplaySampleRate") || 0 %>,
trackInteractions: true,
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+444
View File
@@ -0,0 +1,444 @@
# Release v1.51.0-next.3
Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.51.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.51.0-next.3)
## @backstage/plugin-scaffolder-backend@4.0.0-next.2
### Major Changes
- c78b3b6: Add explicit memory management to SecureTemplater usage
### Minor Changes
- 8006acf: The template parameter schema response now exposes a `formDecorators` field
instead of `EXPERIMENTAL_formDecorators`. Templates that still declare
`spec.EXPERIMENTAL_formDecorators` are read transparently and surfaced under
the new field.
### Patch Changes
- 1ecc3ca: Fixed spelling mistakes in internal code
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/plugin-scaffolder-common@2.2.0-next.1
- @backstage/plugin-scaffolder-node@0.13.3-next.2
## @backstage/integration-aws-node@0.2.0-next.1
### Minor Changes
- 8df06ec: Added `webIdentityTokenFile` to `AwsIntegrationAccountConfig` and
`AwsIntegrationDefaultAccountConfig`. When set along with a `roleName`,
`DefaultAwsCredentialsManager` retrieves credentials by calling
`AssumeRoleWithWebIdentity` (via `fromTokenFile`) using the file's
contents as the web identity token. The file is re-read on each
credential refresh.
The validator rejects combining `webIdentityTokenFile` with
`accessKeyId`/`secretAccessKey`, `profile`, or `externalId`, and
rejects setting it without a `roleName`.
## @backstage/plugin-catalog-backend@3.7.0-next.2
### Minor Changes
- c2de113: **BREAKING**: When paginating entities with an order field via `/entities/by-query`, entities that lack the order field are now excluded from both the result set and the `totalItems` count. Previously these entities appeared at the end of the sorted result via `NULLS LAST`, but cursor-based pagination could not actually reach them past the first page — the count over-reported the number of navigable entities. The new behavior aligns the count with what is actually returned.
This also removes the `DISTINCT` deduplication from the sort-field CTE, which is a prerequisite for the planner to use the `(key, value, entity_id)` index in sort order and short-circuit on `LIMIT`. Installations with duplicate search rows should land the search-table deduplication migration before adopting this change.
### Patch Changes
- ccbad9d: Improved the performance of the `catalog_entities_count` metric.
The legacy Prometheus and OpenTelemetry observable gauges previously each ran their own copy of the per-kind count query against the `search` table on every metrics scrape. On large catalogs this could pile up faster than the queries completed, contending for buffers and stalling the database.
The two callbacks now share a single query result with a short in-process TTL cache, and the underlying query reads from `final_entities` instead of `search`, avoiding the bitmap heap scans that dominated the previous form. The emitted labels and values are unchanged.
- add5d1a: Restructured the entity listing endpoint so that, when a sort field is specified, the search-by-key index drives the query rather than being side-joined onto `final_entities`. This lets PostgreSQL walk the `(key, value, entity_id)` index in already-sorted order and short-circuit on `LIMIT`, reducing typical broad-filter paginated list times from seconds to milliseconds. Entities that lack the sort field still appear at the end of sorted results (NULLS LAST semantics preserved), ordered by `entity_id`.
- 387ea7d: Simplified the entity facets aggregation from `COUNT(DISTINCT entity_id)` to `COUNT(*)`. The unique constraint on `(entity_id, key, value)` guarantees each entity appears at most once per search row group, making the `DISTINCT` unnecessary. This allows the database to use a simpler aggregation plan.
- 3f55b73: Improved the performance of the entity facets endpoint when filters are applied. The filtered entity set is now combined with the search table through an inner join rather than a `WHERE entity_id IN (subquery)`. Results are unchanged; on large catalogs the query planner is able to choose dramatically cheaper plans, with measured improvements ranging from roughly 1.2× on already-fast cases to 7× or more on high-cardinality facets.
- cde3643: Added missing description to the `type` parameter on the `unregister-entity` MCP action.
- 7445f0f: Added a migration that removes duplicate rows from the `search` table, creates covering indices for improved query performance, and adds a `UNIQUE` constraint on `(entity_id, key, value)`.
This is a long-running migration on large catalogs. On PostgreSQL with millions of search rows, the index creation may take 5-15 minutes per index. During this time, other pods running the previous version will continue to serve traffic normally — the index creation does not block reads or writes. However, if a Kubernetes liveness probe kills the pod before the index build completes, the build is lost and the next startup will start over. On large tables this can repeat indefinitely.
**For large installations**, it is recommended to run the following SQL commands against your PostgreSQL database **before deploying** this version. Each index build takes a few minutes but does not block reads or writes. If these have already completed, the migration will detect the existing indices and skip all work — startup will be instant.
```sql
-- Step 1: Remove duplicate search rows
WITH cte AS (
SELECT ctid, row_number() OVER (PARTITION BY entity_id, key, value) AS rn
FROM search
)
DELETE FROM search USING cte WHERE search.ctid = cte.ctid AND cte.rn > 1;
-- Step 2: Create new indices (run each separately)
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS
search_entity_key_value_idx ON search (entity_id, key, value);
CREATE INDEX CONCURRENTLY IF NOT EXISTS
search_key_value_entity_idx ON search (key, value, entity_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS
search_facets_covering_idx ON search (key, original_value, entity_id)
WHERE original_value IS NOT NULL;
-- Step 3: Drop old indices that are no longer needed
DROP INDEX CONCURRENTLY IF EXISTS search_key_value_idx;
DROP INDEX CONCURRENTLY IF EXISTS search_key_original_value_idx;
```
Also fixed `buildEntitySearch` to remove duplicate output for entities with duplicate array values, and added `ON CONFLICT DO UPDATE` to `syncSearchRows` so that concurrent stitching races are handled gracefully.
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
## @backstage/plugin-scaffolder@1.37.0-next.2
### Minor Changes
- dbeb7aa: Added experimental BUI (Backstage UI) form theme for scaffolder forms. All default field extensions render BUI variants when enabled.
**Extension config:**
```yaml
app:
extensions:
- sub-page:scaffolder/templates:
config:
enableBackstageUi: true
```
**JSX props:**
```tsx
<ScaffolderPage formProps={{ EXPERIMENTAL_theme: 'bui' }} />
```
- 8006acf: Promoted `formDecoratorsApiRef`, `ScaffolderFormDecoratorsApi`,
`DefaultScaffolderFormDecoratorsApi`, and `formDecoratorsApi` from `@alpha`
to `@public`.
- d09c21c: The `sub-page:scaffolder/templates` extension now accepts a `groups` config
field that lets you define template groups on the template list page. Each group
has a `title` and a `filter` predicate. Templates not matched by any
configured group fall into an automatically appended "Other Templates" group.
With no groups configured, the page renders a single "Templates" group as
before.
Example:
```yaml
app:
extensions:
- sub-page:scaffolder/templates:
config:
groups:
- title: Recommended Services
filter:
spec.type: service
- title: Documentation
filter:
spec.type: documentation
```
### Patch Changes
- 1ecc3ca: Fixed spelling mistakes in internal code
- 8006acf: Form decorator input is now parsed against the zod schema configured on the
decorator before the decorator runs, so defaults declared via `.default()`
are applied and invalid input is reported through the error API instead of
silently passing through.
- 8006acf: The template wizard now reads form decorators from the new
`spec.formDecorators` field on a template, falling back to the deprecated
`spec.EXPERIMENTAL_formDecorators` for templates that have not been migrated.
- Updated dependencies
- @backstage/plugin-scaffolder-react@1.21.0-next.1
- @backstage/ui@0.15.0-next.3
- @backstage/plugin-scaffolder-common@2.2.0-next.1
- @backstage/plugin-catalog-react@2.1.5-next.1
## @backstage/plugin-scaffolder-common@2.2.0-next.1
### Minor Changes
- 8006acf: Promote the `formDecorators` field on the `Template` spec out of experimental.
The previous `EXPERIMENTAL_formDecorators` field continues to work and is
kept as a deprecated alias.
## @backstage/plugin-scaffolder-react@1.21.0-next.1
### Minor Changes
- dbeb7aa: Added experimental BUI (Backstage UI) form theme for scaffolder forms. All default field extensions render BUI variants when enabled.
**Extension config:**
```yaml
app:
extensions:
- sub-page:scaffolder/templates:
config:
enableBackstageUi: true
```
**JSX props:**
```tsx
<ScaffolderPage formProps={{ EXPERIMENTAL_theme: 'bui' }} />
```
- 8006acf: Promoted `FormDecoratorBlueprint` and `ScaffolderFormDecorator` from `@alpha`
to `@public`.
- d09c21c: The `TemplateCard` component is now a swappable component. Apps using the new
frontend system can replace it by registering a `SwappableComponentBlueprint`
that targets `TemplateCard`. Components used as the swappable implementation
receive `TemplateCardComponentProps`, where `onSelected` is a zero-argument
callback bound to the rendered template. Existing usage continues to work
unchanged.
### Patch Changes
- Updated dependencies
- @backstage/ui@0.15.0-next.3
- @backstage/plugin-scaffolder-common@2.2.0-next.1
- @backstage/plugin-catalog-react@2.1.5-next.1
## @backstage/backend-defaults@0.17.1-next.2
### Patch Changes
- 90b572e: Adds an alpha `TracingService` to provide a unified interface for emitting trace spans across Backstage plugins.
- Updated dependencies
- @backstage/integration-aws-node@0.2.0-next.1
- @backstage/backend-plugin-api@1.9.1-next.1
## @backstage/backend-dynamic-feature-service@0.8.2-next.1
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/backend-defaults@0.17.1-next.2
- @backstage/plugin-catalog-backend@3.7.0-next.2
- @backstage/plugin-scaffolder-node@0.13.3-next.2
## @backstage/backend-plugin-api@1.9.1-next.1
### Patch Changes
- 90b572e: Adds an alpha `TracingService` to provide a unified interface for emitting trace spans across Backstage plugins.
## @backstage/backend-test-utils@1.11.3-next.2
### Patch Changes
- 7fb12b8: Added a new tracing service mock to be leveraged in tests
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/backend-defaults@0.17.1-next.2
## @backstage/create-app@0.8.3-next.3
### Patch Changes
- 14e2056: Pinned the Jest version range in app templates to `~30.2.0` to prevent automatic upgrades to Jest 30.4.x, which requires Node.js v24.9+ and breaks tests on Node 22.
## @backstage/ui@0.15.0-next.3
### Patch Changes
- 4bb649d: Fixed Table with row selection creating phantom scroll height on ancestor elements by establishing a containing block for visually-hidden checkbox inputs.
**Affected components:** Table, TableRoot
- d726bcd: Added new `DatePicker` component — combines a date field and a calendar popover for selecting a date, built on React Aria with full keyboard and screen reader accessibility. Uses BUI design tokens throughout, including auto-incremented backgrounds via the bg consumer pattern.
**Affected components:** DatePicker
## @backstage/plugin-app@0.4.6-next.2
### Patch Changes
- a345820: The `app/routes` redirect config now supports path parameter substitution in the `to` target. Named params (`:userId`) and splat params (`*`) captured by the `from` path are replaced in the `to` string before navigating, making it possible to express redirects like:
```yaml
app:
extensions:
- app/routes:
config:
redirects:
- from: /users/:userId
to: /profile/:userId
- from: /old-docs
to: /docs/*
```
- Updated dependencies
- @backstage/ui@0.15.0-next.3
## @backstage/plugin-auth@0.1.8-next.2
### Patch Changes
- 4f62755: Improved the OAuth consent dialog for MCP authorization by showing more client details, including the client metadata host for CIMD clients, the metadata URL, callback URL, and requested scopes.
- Updated dependencies
- @backstage/ui@0.15.0-next.3
## @backstage/plugin-auth-backend@0.28.1-next.2
### Patch Changes
- 4f62755: Improved the OAuth consent dialog for MCP authorization by showing more client details, including the client metadata host for CIMD clients, the metadata URL, callback URL, and requested scopes.
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
## @backstage/plugin-catalog@2.0.5-next.1
### Patch Changes
- 728629c: Fixed an issue where navigating to an unknown sub-path on an entity page (for example `/catalog/default/component/foo/blob`) would silently render the first available route. Unknown paths now show the standard not-found page instead.
- Updated dependencies
- @backstage/ui@0.15.0-next.3
- @backstage/plugin-scaffolder-common@2.2.0-next.1
- @backstage/plugin-catalog-react@2.1.5-next.1
## @backstage/plugin-catalog-backend-module-aws@0.4.23-next.2
### Patch Changes
- Updated dependencies
- @backstage/integration-aws-node@0.2.0-next.1
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/backend-defaults@0.17.1-next.2
## @backstage/plugin-catalog-backend-module-gitlab@0.8.3-next.2
### Patch Changes
- 1ecc3ca: Fixed spelling mistakes in internal code
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/backend-defaults@0.17.1-next.2
## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.12-next.2
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/backend-defaults@0.17.1-next.2
- @backstage/plugin-catalog-backend@3.7.0-next.2
## @backstage/plugin-catalog-backend-module-logs@0.1.22-next.1
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/plugin-catalog-backend@3.7.0-next.2
## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.20-next.1
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/plugin-scaffolder-common@2.2.0-next.1
## @backstage/plugin-kubernetes-backend@0.21.4-next.1
### Patch Changes
- 1ecc3ca: Fixed spelling mistakes in internal code
- Updated dependencies
- @backstage/integration-aws-node@0.2.0-next.1
- @backstage/backend-plugin-api@1.9.1-next.1
## @backstage/plugin-kubernetes-react@0.5.19-next.1
### Patch Changes
- e68cb8a: Added optional clustersCacheTtlMs option to KubernetesBackendClient that caches getClusters() responses for the specified duration, avoiding repeated /clusters requests when multiple proxy calls resolve cluster auth in quick succession.
## @backstage/plugin-notifications-backend-module-email@0.3.21-next.1
### Patch Changes
- Updated dependencies
- @backstage/integration-aws-node@0.2.0-next.1
- @backstage/backend-plugin-api@1.9.1-next.1
## @backstage/plugin-scaffolder-node@0.13.3-next.2
### Patch Changes
- Updated dependencies
- @backstage/backend-test-utils@1.11.3-next.2
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/plugin-scaffolder-common@2.2.0-next.1
## @backstage/plugin-search-backend-module-elasticsearch@1.8.3-next.2
### Patch Changes
- Updated dependencies
- @backstage/integration-aws-node@0.2.0-next.1
- @backstage/backend-plugin-api@1.9.1-next.1
## @backstage/plugin-techdocs-node@1.15.0-next.2
### Patch Changes
- Updated dependencies
- @backstage/integration-aws-node@0.2.0-next.1
- @backstage/backend-plugin-api@1.9.1-next.1
## example-app@0.0.35-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-app@0.4.6-next.2
- @backstage/plugin-scaffolder-react@1.21.0-next.1
- @backstage/plugin-scaffolder@1.37.0-next.2
- @backstage/plugin-catalog@2.0.5-next.1
- @backstage/ui@0.15.0-next.3
- @backstage/plugin-auth@0.1.8-next.2
- @backstage/plugin-catalog-react@2.1.5-next.1
## example-app-legacy@0.2.121-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-scaffolder-react@1.21.0-next.1
- @backstage/plugin-scaffolder@1.37.0-next.2
- @backstage/plugin-catalog@2.0.5-next.1
- @backstage/ui@0.15.0-next.3
- @backstage/plugin-catalog-react@2.1.5-next.1
## example-backend@0.0.50-next.3
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.9.1-next.1
- @backstage/backend-defaults@0.17.1-next.2
- @backstage/plugin-scaffolder-backend@4.0.0-next.2
- @backstage/plugin-catalog-backend@3.7.0-next.2
- @backstage/plugin-kubernetes-backend@0.21.4-next.1
- @backstage/plugin-auth-backend@0.28.1-next.2
- @backstage/plugin-search-backend-module-elasticsearch@1.8.3-next.2
- @backstage/plugin-catalog-backend-module-logs@0.1.22-next.1
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.20-next.1
## @internal/scaffolder@0.0.21-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-scaffolder-react@1.21.0-next.1
+210
View File
@@ -0,0 +1,210 @@
---
id: v1.51.0
title: v1.51.0
description: Backstage Release v1.51.0
---
These are the release notes for the v1.51.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### **BREAKING**: Removed deprecated `NavItemBlueprint`
The deprecated `NavItemBlueprint` has been removed from `@backstage/frontend-plugin-api`. Navigation items are now discovered from `PageBlueprint` extensions based on their `title` and `icon` params. If you were still using `NavItemBlueprint`, migrate by setting `title` and `icon` on the page extension instead. All built-in plugins have been updated accordingly.
Additionally, `renderInTestApp` from `@backstage/frontend-test-utils` no longer renders a sidebar or legacy `nav-item` extensions. The app nav extension is now disabled in the minimal test app shell.
### **BREAKING**: Removed deprecated `PortableSchema.schema` property form
The deprecated property form of `PortableSchema.schema` has been removed from `@backstage/frontend-plugin-api`. The `schema` member is now a plain method that must be called as `schema()` — direct property access like `schema.type` or `schema.properties` is no longer supported.
### **BREAKING**: Hardened OIDC default patterns
The default allowed patterns for CIMD and DCR in `@backstage/plugin-auth-backend` have been hardened. The previous permissive `['*']` wildcards have been replaced with specific defaults for known MCP clients. If you previously relied on the permissive defaults and have custom MCP clients, you will need to explicitly add their patterns to the allow list.
### **BREAKING**: Cleaned up `PolicyQueryUser` type
The `token` and `expiresInSeconds` fields have been removed from `PolicyQueryUser` in `@backstage/plugin-permission-node`. These were previously deprecated in favor of `credentials` with `coreServices.auth`. The `identity` field has been deprecated. A new `CachedUserInfoService` with a 5-second TTL cache and in-flight request coalescing has been added to reduce repeated user info lookups.
### **BREAKING**: Catalog entity pagination excludes entities without sort field
When paginating entities with an order field via `/entities/by-query`, entities that lack the order field are now excluded from both the result set and the `totalItems` count. Previously these entities appeared at the end via `NULLS LAST`, but cursor-based pagination could not actually reach them past the first page — the count over-reported the number of navigable entities.
### **BREAKING**: Microsoft Graph disabled users filtered by default
The `@backstage/plugin-catalog-backend-module-msgraph` and `@backstage/plugin-catalog-backend-module-msgraph-incremental` providers now filter out disabled user accounts by default. The provider automatically applies an `accountEnabled eq true` filter, combining it with any custom `user.filter` you provide. If you need to ingest disabled accounts, set the filter to explicitly include them.
Contributed by [@mtlewis](https://github.com/mtlewis) in [#34165](https://github.com/backstage/backstage/pull/34165)
### **BREAKING**: Backstage UI updates
There are several new additions in Backstage UI:
**New components:** A `Combobox` component pairs a text input with a filterable dropdown, supporting sectioned options, icons, sizes, and custom typed values. New `DatePicker` and `DateRangePicker` components provide accessible date selection with calendar popovers built on React Aria. Flex item props (`grow`, `shrink`, `basis`) have been added to `Box`, `Card`, `Grid`, and `Flex`.
**Header improvements:** A `sticky` prop has been added to the `Header` component that keeps the title-and-actions bar fixed at the top of its scroll container. New `description`, `tags`, and `metadata` props provide richer header content. The `breadcrumbs` prop has been deprecated.
**Other additions:** Grouped options in `Select`, `isPending` prop replacing `loading` across components, `searchDebounceMs` and `filterDebounceMs` options for `useTable`, `PasswordField` visual alignment with `TextField`, a public `--bui-bg-inherit` CSS variable, and keyboard focus indicators on `Card` links.
**Breaking changes:**
- **Header**: Removed the main header class from the `Header` component. Custom styles targeting this class should be updated.
- `@remixicon/react` dependency limited to versions below 4.9.0 due to a license change.
- React Aria dependencies updated to v1.17.0 and migrated to monopackages.
The `Combobox` was contributed by [@jabrks](https://github.com/jabrks) in [#34118](https://github.com/backstage/backstage/pull/34118). The `DatePicker` was contributed by [@Swiftwork](https://github.com/Swiftwork) in [#34184](https://github.com/backstage/backstage/pull/34184). Flex item props were contributed by [@mtlewis](https://github.com/mtlewis) in [#33948](https://github.com/backstage/backstage/pull/33948).
Check the [BUI Changelog](https://ui.backstage.io/changelog) for more details.
### `AiResource` catalog entity kind
A new `AiResource` catalog entity kind has been introduced, with entity types, validators, type guards, and model layer definitions exported from `@backstage/catalog-model/alpha`. Install `@backstage/plugin-catalog-backend-module-ai-model` to enable it. A new `spec.type: 'mcp-server'` structured subtype has also been added to the `API` kind, with a `spec.remotes` list for representing MCP server connections.
### New plugin: Microsoft Graph incremental ingestion
A new `@backstage/plugin-catalog-backend-module-msgraph-incremental` module provides cursor-based incremental ingestion for Microsoft Graph. Unlike `MicrosoftGraphOrgEntityProvider`, this module never holds the full dataset in memory — each burst processes a single page of up to 999 users or 100 groups. The cursor is persisted so a pod restart resumes from the last completed page.
Contributed by [@sriharsha9618](https://github.com/sriharsha9618) in [#34053](https://github.com/backstage/backstage/pull/34053)
### Scaffolder form decorators promoted to stable
The `formDecorators` field on template specs, the `formDecoratorsApiRef` API, and the `FormDecoratorBlueprint` have been promoted from `@alpha` to `@public` across `@backstage/plugin-scaffolder`, `@backstage/plugin-scaffolder-react`, `@backstage/plugin-scaffolder-common`, and `@backstage/plugin-scaffolder-backend`. The previous `EXPERIMENTAL_formDecorators` field continues to work as a deprecated alias. Decorator input is now validated against the configured zod schema before execution.
### Experimental BUI scaffolder form theme
An experimental Backstage UI form theme has been added for scaffolder forms. All default field extensions render BUI variants when enabled. Set `enableBackstageUi: true` in the `sub-page:scaffolder/templates` extension config to try it out.
### Template groups configuration
The `sub-page:scaffolder/templates` extension now accepts a `groups` config field for defining template groups on the template list page. Each group has a `title` and a `filter` predicate. Templates not matched by any group fall into an "Other Templates" group.
```yaml
app:
extensions:
- sub-page:scaffolder/templates:
config:
groups:
- title: Recommended Services
filter:
spec.type: service
- title: Documentation
filter:
spec.type: documentation
```
The `TemplateCard` is now a swappable component — apps using the new frontend system can replace it via `SwappableComponentBlueprint`.
### `ExtensionPointFactoryMiddleware` for backend
A new `ExtensionPointFactoryMiddleware` type and `createExtensionPointFactoryMiddleware` helper have been added to `@backstage/backend-app-api`, allowing extension point outputs to be replaced at backend creation time. The `defaultServiceFactories` export has been added to `@backstage/backend-defaults` for use with `createSpecializedBackend`.
Contributed by [@UsainBloot](https://github.com/UsainBloot) in [#33782](https://github.com/backstage/backstage/pull/33782)
### Alpha `TracingService`
An alpha `TracingService` has been added to `@backstage/backend-plugin-api` and `@backstage/backend-defaults`, providing a unified interface for emitting trace spans across Backstage plugins. The service includes `context` and `propagation` support for bridging OpenTelemetry context across async boundaries. MCP `tools/call` invocations now emit trace spans following OpenTelemetry server-side MCP semantic conventions. A corresponding mock is available in `@backstage/backend-test-utils`.
Contributed by [@iamEAP](https://github.com/iamEAP) in [#34087](https://github.com/backstage/backstage/pull/34087)
### Catalog performance improvements
Several performance improvements have been made to the catalog backend:
- The entity listing endpoint now lets PostgreSQL walk the `(key, value, entity_id)` index in sorted order and short-circuit on `LIMIT`, reducing typical paginated list times from seconds to milliseconds.
- The entity facets endpoint uses an inner join rather than `WHERE entity_id IN (subquery)` when filters are applied, with measured improvements from ~1.2x to 7x.
- The facets aggregation simplified from `COUNT(DISTINCT entity_id)` to `COUNT(*)`, enabled by the new unique constraint.
- The `catalog_entities_count` metric now shares a single cached query between Prometheus and OpenTelemetry gauges.
- A missing index on `relations.target_entity_ref` has been added, fixing full sequential scans on orphan deletion, entity ancestry, and eager pruning queries.
- Incremental ingestion `WHERE ref IN (...)` queries now use `= ANY($1)` with a single array parameter to reduce prepared statement bloat.
- A new migration removes duplicate rows from the `search` table, creates covering indices, and adds a `UNIQUE` constraint on `(entity_id, key, value)`. **For large installations**, it is recommended to run the provided SQL commands before deploying — see the [changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.51.0-changelog.md) for details.
### New ESLint `no-self-package-imports` rule
A new `no-self-package-imports` lint rule has been added to `@backstage/eslint-plugin`, enabled as `error` in the recommended config. It reports when a package imports itself by its own name instead of using a relative path — a pattern that causes circular initialization errors in bundled ESM and with `jest.requireActual`.
Contributed by [@dyatko](https://github.com/dyatko) in [#34041](https://github.com/backstage/backstage/pull/34041)
### AWS web identity token file support
Added `webIdentityTokenFile` to `@backstage/integration-aws-node` account configuration. When set along with a `roleName`, `DefaultAwsCredentialsManager` retrieves credentials by calling `AssumeRoleWithWebIdentity` using the file's contents as the web identity token. The file is re-read on each credential refresh.
Contributed by [@hudsonb](https://github.com/hudsonb) in [#34149](https://github.com/backstage/backstage/pull/34149)
### TechDocs: disable external font downloads
Added support for disabling external font downloads in TechDocs, useful for air-gapped Backstage instances. Available via the `techdocs.generator.mkdocs.disableExternalFonts` app-config option and the `techdocs-cli generate --disableExternalFonts` CLI flag.
Contributed by [@karthikjeeyar](https://github.com/karthikjeeyar) in [#31838](https://github.com/backstage/backstage/pull/31838)
### `always()` and `failure()` status functions for scaffolder steps
Added `always()` and `failure()` status check functions for scaffolder steps. These can be used in the `if` field to control execution after failures. `always()` ensures a step runs regardless of previous step outcomes, while `failure()` runs a step only when a previous step has failed.
Contributed by [@Ferin79](https://github.com/Ferin79) in [#32890](https://github.com/backstage/backstage/pull/32890)
### Deprecated immediate mode stitching
The `catalog.stitchingStrategy.mode: 'immediate'` setting has been deprecated. A warning is now logged on startup when immediate mode is configured. Immediate mode will be removed in the next Backstage release.
### Additional fixes and improvements
- Fixed scheduler `sleep` firing immediately for durations longer than ~24.8 days, caused by Node.js `setTimeout` overflowing its 32-bit millisecond limit.
- Fixed an issue where navigating to an unknown sub-path on an entity page would silently render the first available route instead of showing a not-found page.
- Fixed widgets not being movable or resizable after saved edits on the home page. Contributed by [@aurnik](https://github.com/aurnik) in [#33721](https://github.com/backstage/backstage/pull/33721).
- Fixed a regression that caused disabled nav items to appear in the navigation bar. Contributed by [@benjidotsh](https://github.com/benjidotsh) in [#33788](https://github.com/backstage/backstage/pull/33788).
- Fixed Valkey cluster mode to use the correct `Cluster` class instead of `createCluster` from `@keyv/redis`. Contributed by [@ganievs](https://github.com/ganievs) in [#33895](https://github.com/backstage/backstage/pull/33895).
- Fixed a race condition in `CachedUserInfoService` where a failed request could incorrectly evict a newer cache entry.
- Fixed `mockCredentials` to include the internal `version: 'v1'` field on all credential objects.
- Fixed filter predicates that mix operator keys (`$all`, `$any`, `$not`) with other keys now being rejected instead of silently dropping conditions.
- Fixed a bug in `PackageGraph.listChangedPackages` where removed dependencies were not detected during lockfile analysis.
- Fixed several database migration `down` functions in the catalog backend that were not properly reversible.
- Fixed a bug causing `--legacyCopyReadmeMdToIndexMd` option to fail if docs directory is not present. Contributed by [@rtar](https://github.com/rtar) in [#33370](https://github.com/backstage/backstage/pull/33370).
- Pinned the Jest version range in app templates to `~30.2.0` to prevent automatic upgrades to Jest 30.4.x, which requires Node.js v24.9+.
- Invalid feature flag declarations no longer crash the app during bootstrap — they are now reported through the error collector and skipped.
- Improved the OAuth consent dialog for MCP authorization by showing more client details. Contributed by [@djamaile](https://github.com/djamaile) in [#34130](https://github.com/backstage/backstage/pull/34130).
- Improved OIDC error messages to include the rejected redirect URI or client ID.
- Refresh token usage now verifies that the user's catalog entity still exists before issuing a new access token. Contributed by [@mtlewis](https://github.com/mtlewis) in [#34142](https://github.com/backstage/backstage/pull/34142).
- Limit the size of fetched client ID metadata documents to prevent oversized responses.
- The notification description in the notifications table is now a swappable component.
- Added scope-based Slack message update support for the notifications backend. Contributed by [@erikmiller-gusto](https://github.com/erikmiller-gusto) in [#33649](https://github.com/backstage/backstage/pull/33649).
- Added a search backend action for querying the search engine via the actions registry. Contributed by [@drodil](https://github.com/drodil) in [#31010](https://github.com/backstage/backstage/pull/31010).
- Added MCP `tools/call` trace spans following OpenTelemetry semantic conventions. Contributed by [@iamEAP](https://github.com/iamEAP) in [#34089](https://github.com/backstage/backstage/pull/34089).
- Scheduled Tasks page in DevTools now refreshes automatically after a task is triggered or cancelled. Contributed by [@officialasishkumar](https://github.com/officialasishkumar) in [#34049](https://github.com/backstage/backstage/pull/34049).
- Migrated `ConfigContent` component in DevTools from Material UI to Backstage UI. Contributed by [@AdityaK60](https://github.com/AdityaK60) in [#33252](https://github.com/backstage/backstage/pull/33252).
- Scaffolder list-tasks action now supports a `status` filter parameter. Contributed by [@johnmcollier](https://github.com/johnmcollier) in [#33122](https://github.com/backstage/backstage/pull/33122).
- Added `allowEmpty` input option to the `gitlab:repo:push` scaffolder action. Contributed by [@elaine-mattos](https://github.com/elaine-mattos) in [#33602](https://github.com/backstage/backstage/pull/33602).
- Improved Octokit client creation in the GitHub scaffolder module to support retries. Contributed by [@adobejmong](https://github.com/adobejmong) in [#34027](https://github.com/backstage/backstage/pull/34027).
- Added optional `clustersCacheTtlMs` option to `KubernetesBackendClient` for caching cluster responses. Contributed by [@alde](https://github.com/alde) in [#34136](https://github.com/backstage/backstage/pull/34136).
- Prioritized i18n translations over `theme.title` for theme names in user settings. Contributed by [@its-mitesh-kumar](https://github.com/its-mitesh-kumar) in [#34113](https://github.com/backstage/backstage/pull/34113).
- Added experimental support for checking suspended GitHub users via REST API. Contributed by [@mtlewis](https://github.com/mtlewis) in [#34300](https://github.com/backstage/backstage/pull/34300).
- The `GithubMultiOrgEntityProvider` now emits entities in a stable order during full mutations.
- Added permission authorization checks to the unprocessed entities read endpoints.
- Upgraded Module Federation packages to v2.3.3 to address known vulnerabilities. Contributed by [@secustor](https://github.com/secustor) in [#33949](https://github.com/backstage/backstage/pull/33949).
- Removed the `uuid` dependency across many packages, replacing it with the built-in `crypto.randomUUID()`.
## Security Fixes
This release does not contain any security fixes.
## Contributors
Big shoutout to all 34 of you amazing folks who chipped in on this release: [@AdityaK60](https://github.com/AdityaK60), [@Ferin79](https://github.com/Ferin79), [@Naycon](https://github.com/Naycon), [@Swiftwork](https://github.com/Swiftwork), [@UsainBloot](https://github.com/UsainBloot), [@adobejmong](https://github.com/adobejmong), [@alde](https://github.com/alde), [@aurnik](https://github.com/aurnik), [@benjidotsh](https://github.com/benjidotsh), [@cdedreuille](https://github.com/cdedreuille), [@copilot-swe-agent](https://github.com/copilot-swe-agent), [@davidjosefson-neo4j](https://github.com/davidjosefson-neo4j), [@deepthi-28](https://github.com/deepthi-28), [@djamaile](https://github.com/djamaile), [@drodil](https://github.com/drodil), [@dyatko](https://github.com/dyatko), [@elaine-mattos](https://github.com/elaine-mattos), [@emmaindal](https://github.com/emmaindal), [@erikmiller-gusto](https://github.com/erikmiller-gusto), [@etienne-napoleone](https://github.com/etienne-napoleone), [@ganievs](https://github.com/ganievs), [@hudsonb](https://github.com/hudsonb), [@iamEAP](https://github.com/iamEAP), [@its-mitesh-kumar](https://github.com/its-mitesh-kumar), [@jabrks](https://github.com/jabrks), [@johnmcollier](https://github.com/johnmcollier), [@jtbry](https://github.com/jtbry), [@karthikjeeyar](https://github.com/karthikjeeyar), [@mtlewis](https://github.com/mtlewis), [@officialasishkumar](https://github.com/officialasishkumar), [@rtar](https://github.com/rtar), [@secustor](https://github.com/secustor), [@sriharsha9618](https://github.com/sriharsha9618), [@wss-dogara](https://github.com/wss-dogara)
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.51.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://spoti.fi/backstagenewsletter) if you want to be informed about what is happening in the world of Backstage.