Playlist logo

Backstage Playlist Plugin

Created by Phil Kuang

Playlist lets you create, share, and follow collections of catalog entities in Backstage. Think of it as saved sets of services, websites, docs, or any entity type. You group what matters, keep it in one place, and come back to it fast. You can keep a playlist private or share it with your team. You can follow playlists from others to stay in sync.

The plugin adds an index where engineers browse, search, and sort playlists. Each playlist has a page with its description and its entities. You can create a new playlist, edit details, or remove one when it is no longer useful. Adding entities is simple. Do it from the playlist page or from an entity page using the add to playlist dialog. It reads from your catalog so the contents stay current.

Common uses include onboarding sets for new hires, runbooks for incident response, release or migration trackers, or collections for audits. Platform teams use it to highlight golden paths. Individual engineers use it to pin the services they touch every week. You can customize the term shown in the UI if you prefer a name like Collections. You can also compose your own index view to match your portal. The goal is simple. Help your teams discover and organize the things they use most.

Installation Instructions

These instructions apply to self-hosted Backstage only.

Install the frontend package

Run this in your Backstage root

Copy
yarn --cwd packages/app add @backstage-community/plugin-playlist

Register the frontend API client

Wire the client in your app apis file

Copy
// packages/app/src/apis.ts
import {
  createApiFactory,
  discoveryApiRef,
  fetchApiRef,
} from '@backstage/core-plugin-api';
import {
  playlistApiRef,
  PlaylistClient,
} from '@backstage-community/plugin-playlist';

export const apis = [
  // keep your existing factories

  createApiFactory({
    api: playlistApiRef,
    deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
    factory: ({ discoveryApi, fetchApi }) =>
      new PlaylistClient({ discoveryApi, fetchApi }),
  }),
];

Add routes to your app

Import the pages then add the routes

Copy
// packages/app/src/App.tsx
import React from 'react';
import { FlatRoutes, Route } from '@backstage/core-app-api';
import { CatalogIndexPage, CatalogEntityPage } from '@backstage/plugin-catalog';
import {
  PlaylistIndexPage,
  PlaylistPage,
} from '@backstage-community/plugin-playlist';

export const App = () => (
  <FlatRoutes>
    <Route path="/catalog" element={<CatalogIndexPage />} />
    <Route
      path="/catalog/:namespace/:kind/:name"
      element={<CatalogEntityPage />}
    />
    <Route path="/playlist" element={<PlaylistIndexPage />} />
    <Route path="/playlist/:playlistId" element={<PlaylistPage />} />
  </FlatRoutes>
);
Copy
// packages/app/src/components/Root/Root.tsx
import React, { PropsWithChildren } from 'react';
import { Sidebar, SidebarItem, SidebarPage } from '@backstage/core-components';
import PlaylistPlayIcon from '@material-ui/icons/PlaylistPlay';

export const Root = ({ children }: PropsWithChildren<{}>) => (
  <SidebarPage>
    <Sidebar>
      <SidebarItem icon={PlaylistPlayIcon} to="playlist" text="Playlists" />
      {children}
    </Sidebar>
  </SidebarPage>
);

Add the entity page menu to support add to playlist

Imports

Copy
// packages/app/src/components/catalog/EntityPage.tsx
import React, { ReactNode, useMemo, useState } from 'react';
import { EntityLayout } from '@backstage/plugin-catalog';
import { EntityPlaylistDialog } from '@backstage-community/plugin-playlist';
import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd';

Wrapper

Copy
// place this near the top of EntityPage.tsx after imports
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
  const [playlistDialogOpen, setPlaylistDialogOpen] = useState(false);

  const extraMenuItems = useMemo(
    () => [
      {
        title: 'Add to playlist',
        Icon: PlaylistAddIcon,
        onClick: () => setPlaylistDialogOpen(true),
      },
    ],
    [],
  );

  return (
    <>
      <EntityLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
        {props.children}
      </EntityLayout>
      <EntityPlaylistDialog
        open={playlistDialogOpen}
        onClose={() => setPlaylistDialogOpen(false)}
      />
    </>
  );
};

Wrap your entity routes

Copy
// still in EntityPage.tsx
const defaultEntityPage = (
  <EntityLayoutWrapper>
    <EntityLayout.Route path="/" title="Overview">
      {overviewContent}
    </EntityLayout.Route>

    <EntityLayout.Route path="/docs" title="Docs">
      <EntityTechdocsContent />
    </EntityLayout.Route>

    <EntityLayout.Route path="/todos" title="TODOs">
      <EntityTodoContent />
    </EntityLayout.Route>
  </EntityLayoutWrapper>
);

Optional config for a custom title

Copy
# app-config.yaml
playlist:
  title: Collection

Install the backend plugin

Add the backend package

Copy
yarn --cwd packages/backend add @backstage-community/plugin-playlist-backend

New backend system

Register the module in your backend index

Copy
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// other backend modules you already add
// example
// backend.add(import('@backstage/plugin-catalog-backend'));

backend.add(import('@backstage-community/plugin-playlist-backend'));

backend.start();

This exposes the service at the playlist discovery service id. The frontend client uses discovery to reach it.

Old backend system

Create the plugin router

Copy
// packages/backend/src/plugins/playlist.ts
import { createRouter } from '@backstage-community/plugin-playlist-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';

export default async function createPlugin(
  env: PluginEnvironment,
): Promise<Router> {
  return await createRouter({
    logger: env.logger,
    database: env.database,
    config: env.config,
  });
}

Wire the router

Copy
// packages/backend/src/index.ts
import playlist from './plugins/playlist';

// inside your main bootstrap where apiRouter is created
const playlistEnv = useHotMemoize(module, () => createEnv('playlist'));
apiRouter.use('/playlist', await playlist(playlistEnv));

// keep the rest of your routers
// serviceBuilder.addRouter('/api', apiRouter) already exists in most apps

Now the backend serves at the api playlist path. The frontend client calls it through discovery.

Changelog

This changelog is produced from commits made to the Playlist plugin since a year ago, and based on the code located here. It may not contain information about all commits. Releases and version bumps are intentionally omitted. This changelog is generated by AI.

Performance

  • Improve playlist entity query speed by using getEntitiesByRefs. Large playlists load much faster. #4010 merged 4 months ago

Documentation

  • Update README links to point to the community plugins repository. #3931 merged 4 months ago

Dependencies

  • Update types uuid to v10 in dev dependencies. #2233 merged 7 months ago
  • Remove unused dev dependency canvas. #3565 merged 5 months ago

Maintenance

  • Reduce knip false positives by updating repo tools config. #3018 merged 6 months ago
  • Remove usages of backstage backend common in the playlist plugin. #1958 merged 10 months ago

Set up Backstage in minutes with Roadie