Certified Backstage Associate study materials
Certified Backstage Associate study resources

The Tab That Should Not Appear for Resources

Certified Backstage Associate · Put your knowledge to work in context.

All case studies

The Tab That Should Not Appear for Resources

Northwind's Backstage app has 14 plugins and a customized entity page in packages/app/src/components/catalog/EntityPage.tsx. The platform team wants a new Deployments tab that appears only for kind: Component with spec.type: service and the annotation backstage.io/kubernetes-id. The tab must call https://deployments.internal.northwind.example/api/v1/services/{name} and display the latest rollout. The API requires a bearer token that must not reach the browser. They also want to hide the TechDocs tab for kind: Resource, and add a scaffolder action that creates a Kubernetes namespace. The action must be idempotent because template retries happen, and it must not accept the cluster token as a template parameter. Two teams already maintain forked copies of the default entity page, and upgrades are painful. The app uses the legacy frontend system today, with a plan to evaluate the new frontend system next quarter.

1. Implement the conditional Deployments tab without forking the entity page further. What code structure and predicate would you use, and how would you keep it upgrade-safe?

Read the worked answer

Keep EntityPage.tsx as composition, not as a fork. Define a predicate such as const isDeployableService = (entity) => entity.kind === 'Component' && entity.spec?.type === 'service' && Boolean(entity.metadata.annotations?.['backstage.io/kubernetes-id']); then add an EntityLayout.Route with path='/deployments', title='Deployments', and if={isDeployableService}. For TechDocs, add the inverse predicate on the existing route rather than copying the default page. Put the tab component in a local plugin package so the entity page only imports it. When you move to the new frontend system, express the same predicate as an entity-content extension filter instead of editing a central page. Add tests for component/service, component/website, and resource cases so the tab does not leak. This keeps your customizations as small overlays, which makes upstream upgrades less painful.

2. The Deployments tab must call a token-protected internal API. Show the request path and explain why the Backstage proxy is or is not enough.

Read the worked answer

Do not call the internal API directly from the browser. Add a proxy endpoint such as /deployments with the target https://deployments.internal.northwind.example/api/v1, set the bearer token in the proxy headers from a backend secret, and have the tab call /api/proxy/deployments/services/<name> through fetchApi. The token stays server-side and CORS is avoided. But the proxy is a shared credential: every Backstage user can reach everything the token can. If the API needs per-user authorization, write a small backend plugin that authenticates the Backstage user, checks a permission, and forwards a scoped token. For the proxy, restrict methods, set a timeout, and return sanitized errors. Never put the token in frontend-visible configuration, and do not rely on the proxy as an authorization boundary.

3. Design the custom scaffolder action for namespace creation. What makes it idempotent, and where does the cluster token come from?

Read the worked answer

Implement a createTemplateAction in a backend module. Define the input schema as namespace, team, and optional dry-run; do not include the cluster token. Read the token and API server from backend config or a Kubernetes service-account credential, and enforce least-privilege RBAC. Make it idempotent by reading the namespace first; if it exists and is owned by the expected team, return success; if creation returns 409 AlreadyExists, treat it as success after verifying labels. Use ctx.checkpoint so retries after a worker failure do not repeat side effects, log the namespace but never the token, and set a bounded timeout. Emit outputs for the namespace and server URL. Test the retry path by forcing a failure after creation, and test the conflict path with a namespace owned by another team.