Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# AI Agent Instructions for Backstage Showcase

## Project Overview

This is a **Backstage developer portal** — a monorepo with a React frontend (`packages/app`) and Node.js backend (`packages/backend`), plus custom plugins in `plugins/`. It uses the new Backstage backend system with `createBackend()`.

## Development Commands

All commands go through **mise** (not yarn directly):

```bash
mise run install # Install dependencies (runs yarn install)
mise run dev # Start frontend (localhost:3000) + backend (localhost:7007)
mise run lint # Lint and type-check
mise run test # Run tests
mise run build # Build Docker image
mise run up # Start with Traefik (production-like)
```

## Architecture

### Monorepo Structure

- **`packages/app`** — React frontend using MUI v5 + `@backstage/core-components`
- **`packages/backend`** — Node.js backend using `@backstage/backend-defaults`
- **`plugins/`** — Custom plugins (scoped as `@internal/`):
- `plausible/` — Analytics integration (frontend-plugin)
- `permission-backend-module-default/` — RBAC policy (backend-plugin-module)

### Backend Plugin Registration

Backend uses the new system. Add plugins in [packages/backend/src/index.ts](packages/backend/src/index.ts):

```typescript
const backend = createBackend();
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
import('@internal/backstage-plugin-permission-backend-module-default'),
);
backend.start();
```

### Frontend App Structure

- Entry: [packages/app/src/App.tsx](packages/app/src/App.tsx)
- Routes use `FlatRoutes` with `<Route>` components
- Entity pages: [packages/app/src/components/catalog/EntityPage.tsx](packages/app/src/components/catalog/EntityPage.tsx)
- Sidebar navigation: [packages/app/src/components/Root/Root.tsx](packages/app/src/components/Root/Root.tsx)

## Theming Pattern

Custom themes use `createUnifiedTheme()` from `@backstage/theme`:

- Default theme: [packages/app/src/theme/index.ts](packages/app/src/theme/index.ts)
- Brand themes: [packages/app/src/themes/brandTheme.ts](packages/app/src/themes/brandTheme.ts)

Themes are registered in App.tsx via the `themes` array with `UnifiedThemeProvider`.

## Configuration Files

- `app-config.yaml` — Base config (local development)
- `app-config.local.yaml` — Local overrides (gitignored)
- `app-config.production.yaml` — Production config (uses env vars like `${APP_BASE_URL}`)

Environment variables are loaded from `.env` via mise.

## Permission System

RBAC is implemented in [plugins/permission-backend-module-default/src/module.ts](plugins/permission-backend-module-default/src/module.ts):

- Groups: `readers` (read-only), `developers` (CRUD), `admins`
- Uses `policyExtensionPoint` to register custom `PermissionPolicy`

## Software Templates

Templates live in `examples/templates/`. Each template has:

- `template.yaml` — Scaffolder definition with parameters and steps
- `template/` — Skeleton files to copy

Example: [examples/templates/github-blank-repo/template.yaml](examples/templates/github-blank-repo/template.yaml)

## Entity Catalog

- Sample entities: `examples/` directory (components, APIs, domains, systems)
- Entity types customized per kind in [EntityPage.tsx](packages/app/src/components/catalog/EntityPage.tsx)
- Uses `EntitySwitch.Case` with predicates like `isKind('component')`

## Docker & Deployment

- Multi-stage Dockerfile uses asdf for Node.js/Python
- Production runs: `node . --config ../../app-config.yaml --config ../../app-config.production.yaml`
- Traefik reverse proxy via `compose.yaml`

## Key Conventions

1. **Plugin naming**: `@internal/backstage-plugin-{name}` for private plugins
2. **Internal links**: Use `link:../app` in package.json for local dependencies
3. **MUI imports**: Use `@mui/material` (v5), not `@material-ui/core` (v4)
4. **Icons**: Import from `@mui/icons-material` as `IconComponent` for Sidebar
5. **Yarn workspaces**: `packages/*` and `plugins/*`
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ run = './scripts/lint'

[tasks.test]
description = "Run the test suite"
run = 'yarn test'
run = 'yarn test --watchAll=false'

[tasks.build]
description = "Build the Backstage Docker image"
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"resolutions": {
"@types/react": "^18",
"@types/react-dom": "^18",
"@backstage/app-defaults": "^1.7.3",
"@backstage/frontend-app-api": "^0.13.3",
"@backstage/core-app-api": "^1.19.3",
"isolated-vm": "5.0.4"
},
"prettier": "@spotify/prettier-config",
Expand Down
5 changes: 4 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@
"@emotion/styled": "^11.11.5",
"@fontsource/inter": "^5.0.18",
"@internal/backstage-plugin-plausible": "^0.1.0",
"@internal/backstage-theme-github": "link:../backstage-theme-github",
"@mui/icons-material": "^5.15.19",
"@mui/joy": "^5.0.0-beta.36",
"@mui/material": "^5.15.19",
"@mui/styles": "^5.18.0",
"@mui/utils": "^5.15.14",
"@mui/x-charts": "^8.0.0",
"@primer/octicons-react": "^19.21.1",
"@primer/react": "^38.6.2",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
Expand Down Expand Up @@ -87,4 +90,4 @@
"files": [
"dist"
]
}
}
30 changes: 29 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ import { HomePage } from './components/home/HomePage';
import CssBaseline from '@mui/material/CssBaseline';
import { PlausibleAnalytics } from '@internal/backstage-plugin-plausible';
import { githubAuthApiRef, gitlabAuthApiRef } from '@backstage/core-plugin-api';
import {
brandDarkTheme,
brandLightTheme,
} from '@internal/backstage-theme-github';
import { PrimerDemoPage } from './components/PrimerDemoPage';

const app = createApp({
apis,
Expand Down Expand Up @@ -89,9 +94,31 @@ const app = createApp({
),
},
themes: [
{
id: 'brand-light',
title: 'GitHub Light',
variant: 'light',
Provider: ({ children }) => (
<UnifiedThemeProvider theme={brandLightTheme}>
<CssBaseline />
{children}
</UnifiedThemeProvider>
),
},
{
id: 'brand-dark',
title: 'GitHub Dark',
variant: 'dark',
Provider: ({ children }) => (
<UnifiedThemeProvider theme={brandDarkTheme}>
<CssBaseline />
{children}
</UnifiedThemeProvider>
),
},
{
id: 'default',
title: 'Default',
title: 'Legacy (Original)',
variant: 'light',
icon: <AcUnitIcon />,
Provider: ({ children }) => (
Expand All @@ -107,6 +134,7 @@ const app = createApp({
const routes = (
<FlatRoutes>
<Route path="/" element={<HomePage />} />
<Route path="/primer-demo" element={<PrimerDemoPage />} />
<Route
path="/catalog"
element={
Expand Down
41 changes: 41 additions & 0 deletions packages/app/src/components/PrimerDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useTheme as useMuiTheme } from '@mui/material/styles';
import { BaseStyles, Button, Label, ThemeProvider } from '@primer/react';

export function PrimerDemo() {
const muiTheme = useMuiTheme();
const colorMode = muiTheme.palette.mode === 'dark' ? 'night' : 'day';

return (
<ThemeProvider colorMode={colorMode}>
<BaseStyles>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
padding: 16,
borderWidth: 1,
borderStyle: 'solid',
borderColor: 'var(--borderColor-default, rgba(27, 31, 36, 0.16))',
borderRadius: 12,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
}}
>
<Button variant="primary">Primer Button</Button>
<Label variant="accent">Primer Label</Label>
</div>
<p style={{ margin: 0, opacity: 0.8 }}>
Primer BaseStyles + ThemeProvider are scoped to this card only.
</p>
</div>
</BaseStyles>
</ThemeProvider>
);
}
18 changes: 18 additions & 0 deletions packages/app/src/components/PrimerDemoPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Content, Header, InfoCard, Page } from '@backstage/core-components';
import { PrimerDemo } from './PrimerDemo';

export function PrimerDemoPage() {
return (
<Page themeId="home">
<Header
title="Primer Demo"
subtitle="Isolated proof-of-integration for @primer/react"
/>
<Content>
<InfoCard title="Primer components (scoped)">
<PrimerDemo />
</InfoCard>
</Content>
</Page>
);
}
79 changes: 35 additions & 44 deletions packages/app/src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { PropsWithChildren } from 'react';
import { styled } from '@mui/material/styles';
import Home from '@mui/icons-material/Home';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Expand All @@ -9,6 +8,18 @@ import {
} from '@backstage/plugin-user-settings';
import { SidebarSearchModal } from '@backstage/plugin-search';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
import {
HomeIcon,
SearchIcon,
ThreeBarsIcon,
RepoIcon,
PlugIcon,
BookIcon,
RocketIcon,
ChecklistIcon,
PulseIcon,
TelescopeIcon,
} from '@primer/octicons-react';
import {
Sidebar,
sidebarConfig,
Expand All @@ -21,15 +32,6 @@ import {
Link,
SidebarExpandButton,
} from '@backstage/core-components';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
import ExtensionRoundedIcon from '@mui/icons-material/ExtensionRounded';
import MenuBookRoundedIcon from '@mui/icons-material/MenuBookRounded';
import LocalLibraryRoundedIcon from '@mui/icons-material/LocalLibraryRounded';
import TouchAppRoundedIcon from '@mui/icons-material/TouchAppRounded';
import MonitorHeartRoundedIcon from '@mui/icons-material/MonitorHeartRounded';
import ScoreRoundedIcon from '@mui/icons-material/ScoreRounded';
import { IconComponent } from '@backstage/core-plugin-api';

export enum LocalStorageKeys {
Expand Down Expand Up @@ -63,6 +65,15 @@ const SidebarLogo = () => {
);
};

const HomeOcticon: IconComponent = () => <HomeIcon size={20} />;
const RepoOcticon: IconComponent = () => <RepoIcon size={20} />;
const PlugOcticon: IconComponent = () => <PlugIcon size={20} />;
const BookOcticon: IconComponent = () => <BookIcon size={20} />;
const RocketOcticon: IconComponent = () => <RocketIcon size={20} />;
const ChecklistOcticon: IconComponent = () => <ChecklistIcon size={20} />;
const PulseOcticon: IconComponent = () => <PulseIcon size={20} />;
const TelescopeOcticon: IconComponent = () => <TelescopeIcon size={20} />;

export const Root = ({ children }: PropsWithChildren<{}>) => {
if (
window.localStorage.getItem(LocalStorageKeys.SIDEBAR_PIN_STATE) === null
Expand All @@ -77,52 +88,32 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
<SidebarPage>
<Sidebar disableExpandOnHover={false}>
<SidebarLogo />
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
<SidebarGroup
label="Search"
icon={<SearchIcon size={16} />}
to="/search"
>
<SidebarSearchModal />
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
<SidebarGroup label="Menu" icon={<ThreeBarsIcon size={16} />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={Home as IconComponent} to="/" text="Home" />
<SidebarItem
icon={MenuBookRoundedIcon as IconComponent}
to="catalog"
text="Catalog"
/>
<SidebarItem
icon={ExtensionRoundedIcon as IconComponent}
to="api-docs"
text="APIs"
/>
<SidebarItem
icon={LocalLibraryRoundedIcon as IconComponent}
to="docs"
text="Docs"
/>
<SidebarItem icon={HomeOcticon} to="/" text="Home" />
<SidebarItem icon={RepoOcticon} to="catalog" text="Catalog" />
<SidebarItem icon={PlugOcticon} to="api-docs" text="APIs" />
<SidebarItem icon={BookOcticon} to="docs" text="Docs" />
<SidebarItem
icon={TouchAppRoundedIcon as IconComponent}
icon={RocketOcticon}
to="self-service"
text="Self-Service"
/>
</SidebarGroup>
<SidebarDivider />
<NotificationsSidebarItem />
<SidebarDivider />
<SidebarItem
icon={ScoreRoundedIcon as IconComponent}
to="scorecard"
text="Scorecard"
/>
<SidebarItem
icon={MonitorHeartRoundedIcon as IconComponent}
to="pulse-check"
text="Pulse Check"
/>
<SidebarItem
icon={AutoAwesomeRoundedIcon as IconComponent}
to="explore"
text="Explore"
/>
<SidebarItem icon={ChecklistOcticon} to="scorecard" text="Scorecard" />
<SidebarItem icon={PulseOcticon} to="pulse-check" text="Pulse Check" />
<SidebarItem icon={TelescopeOcticon} to="explore" text="Explore" />
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
Expand Down
Loading
Loading