Backstage Custom Field Extension: Dynamic Field Updates

Backstage makes building developer portals a breeze — especially with Software Templates for scaffolding services. But what if you want to build dynamic forms?

For example:

“Pick an AWS account, and auto-select the right IAM role or environment based on the account.”

Let’s walk through how I built an AwsAccountPicker — a custom field extension that listens to changes in another field and updates itself accordingly.

I am using FieldProps (from @rjsf/utils) instead of the Backstage-specific FieldExtensionComponentProps

Why FieldProps? While Backstage templates usually encourage using FieldExtensionComponentProps, using FieldProps gives you full access to the underlying JSON Schema Form engine — giving you more control, especially for dynamic behaviours like reacting to other fields.

Requirement

A custom field extension (AwsAccountPicker) that:

  • Fetches a list of AWS accounts.
  • Observes another form field (like serviceName) via formContext.formData.
  • Dynamically selects the appropriate account based on a pattern match.
Setting Up the Field Extension

In Backstage frontend app/src/components/scaffolder/customScaffolderExtensions.tsx:

import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import { AwsAccountPicker } from './AwsAccountPicker';

export const awsAccountPickerExtension: FieldExtensionOptions = {
  name: 'AwsAccountPicker',
  component: AwsAccountPicker,
};

Inside app/src/components/scaffolder/AwsAccountPicker.tsx:

import React, { useEffect, useMemo } from 'react';
import { FieldProps } from '@rjsf/utils';
import { TextField, MenuItem } from '@material-ui/core';

export const AwsAccountPicker = ({
  formData,
  onChange,
  uiSchema,
  registry,
  schema,
}: FieldProps) => {
  const allFormData = registry.formContext?.formData ?? {};
  const uiOptions = uiSchema['ui:options'] || {};

  const accounts = [
    { id: '123456789012', name: 'dev-account' },
    { id: '210987654321', name: 'prod-account' },
  ]; # Use a hook to get this dynamically from an api

  const valueFrom = uiOptions.autofillFrom;
  const outputType = uiOptions.output || 'id';
  const serviceName = allFormData[valueFrom];

  useEffect(() => {
    if (!serviceName) return;
    const expectedAccount = `${serviceName}-account`;
    const match = accounts.find(acc =>
      expectedAccount.toLowerCase().includes(acc.name.toLowerCase())
    );
    if (match && formData !== match[outputType]) {
      onChange(match[outputType]);
    }
  }, [serviceName, formData, onChange, outputType]);

  return (
    <TextField
      select
      label={schema.title}
      value={formData ?? ''}
      onChange={e => onChange(e.target.value)}
    >
      {accounts.map(account => (
        <MenuItem key={account.id} value={account[outputType]}>
          {account.name}
        </MenuItem>
      ))}
    </TextField>
  );
};

Above is a simplified version of the component using FieldProps

How this Works
  • registry.formContext.formData gives you access to other form fields.
  • uiSchema['ui:options'].autofillFrom defines which field to observe.
  • useEffect triggers when the observed field changes and auto-updates the current field.

This approach is clean, does not require extra state management, and works naturally with Backstage scaffolder templates.

Using It in a Template

In template.yaml:

parameters:
  - title: Service Details
    required:
      - serviceName
      - awsAccount
    properties:
      serviceName:
        type: string
        title: Service Name
      awsAccount:
        title: AWS Account
        type: string
        ui:field: AwsAccountPicker
        ui:options:
          autofillFrom: serviceName
          output: id

With this setup, if serviceName = my-service, and your AWS accounts include my-service-account, the form will auto-select the correct account.

The same pattern can be extended to:

  • Conditionally fetch data based on other fields.
  • Support custom inputs with validation.
  • Handle grouped permissions, environments, or region selectors.

Empowering Developers & Streamlining Workflows with Backstage 

In today’s fast-paced digital landscape, efficiency and consistency are paramount for any organization striving to stay ahead of the curve.  

This becomes more crucial in a cross-located and dynamic team setup. Comes in Backstage – A powerful platform designed to centralize service catalog and standardize service and infrastructure creation. 

Backstage, originally developed by Spotify, is an open platform for building developer portals. It unifies infrastructure tooling, services, and documentation to create a streamlined development environment from end to end. 

Why Backstage? 

Backstage offers a comprehensive solution to the challenges we face in managing our ever-expanding ecosystem of services, APIs, and infrastructure. By centralizing service catalog, Backstage provides a unified view of all internal tools and resources, enabling teams to easily discover, consume, and share services across the organization. This not only promotes transparency but also facilitates collaboration and knowledge sharing among teams. 

Furthermore, Backstages’ templating capabilities can revolutionize the way we create and manage services and infrastructure. With customizable templates, we can standardize project setups, ensuring consistency and reducing the time and effort required to onboard new services. This standardization improves efficiency and enhances system reliability and maintainability. 

Journey towards Centralization and Standardization 

The decision to adopt Backstage is often based on the commitment to excellence and continuous improvement. As teams grow, so does the complexity of managing services and infrastructure. Thus comes the need for a centralized platform that can provide visibility and control, over the entire ecosystem while also promoting best practices and standardization. 

With Backstage, we can take a proactive approach to address these challenges. By centralizing service catalog, we are breaking down silos and creating a culture of collaboration and innovation. Teams can now easily discover and leverage existing services, reducing duplication of efforts, and accelerating time-to-market for new services. 

Moreover, by using templates to standardize service and infrastructure creation, we can ensure consistency and reliability across the organization. Whether it is deploying a new microservice or provisioning a cloud infrastructure, teams can rely on predefined templates to guide them through the process, eliminating guesswork and reducing the risk of errors. 

First things First 

Backstage can be overwhelming to start with and involves some initial effort to get started. Also, as it provides a plugin-based framework, supporting custom requirements becomes easier. Focus on the core features first would involve less time for set-up and provide immediate value to developers. 

The features that could be rolled out initially: 

  1. Centralized service catalog with updated and relevant service metadata – A central service catalog. The most important part here is to ensure that the service metadata is relevant and useful, with proper ownerships set, along with having an effortless way to register to the service catalog. 
  1. Standardized service/infrastructure scaffolding – Standardizing and templatizing service creation and infrastructure has a cascading effect on managing services more efficiently, cost optimization, visibility, and operational excellence. The best part of templates is they are easy to create, can be re-used and enforce standards out of the box when setup correctly. 
  1. Overview of the tech ecosystem (aka TechRadar) – The technology ecosystem and overview are very crucial to understanding how we leverage various tools/frameworks, providing better visibility, take decisions on streamlining and identify/evaluate what is important. 

Conclusion 

Adoption of Backstage represents a significant milestone in the journey towards centralization and standardization. By embracing this powerful platform, we can not only improve development workflows but also lay the foundation for future growth and scalability. The possibilities that Backstage brings and the impact it can have on an organization is  huge.

Also promoting a collaborative model, where developers feel empowered, contribute towards Backstage plugin development, and keep improving the offerings is essential.

Stay tuned for more updates on Backstage and how we can set it up in a production environment.

Backstage – Authorization

Backstage by Spotify is a platform to build Dev Portal for your organisation. The tool provides a robust framework which can be used to customize and create a Dev Portal, custom fit – as I’d like to say 😉

The tool is evolving rapidly with new plugins/features being added quite often. Recently I worked to implement custom Authorization to access Backstage, which I found really interesting and thus am sharing my experience on how I did it.

We have Backstage hosted in K8s (EKS) with GitOps based deployments managed through ArgoCD. We also have Templates (with custom scaffolder actions) to provision new microservices. While setting up the AuthZ, granular access to these Templates was one of our requirements and we wanted to manage its access in a proper way.

Backstage has several AuthN integrations out of the box and it’s pretty easy to set that up. In our case we use OneLogin for AuthN. For AuthZ, Backstage provides a permissions plugin. I am not discussing how to configure and setup that up as it’s documented well.

An authenticated user gets allotted some default user groups. To have a proper group based AuthZ the first thing we did was to develop a custom sign-in resolver to set custom groups for authenticated users.

// auth.ts custom login snippet
onelogin: providers.onelogin.create({
        signIn: {
          // Custom sign-in resolver
          async resolver({ result }, ctx) {
            const email = result.fullProfile.username ?? '';
            const [id] = email.split('@');
            let entity:any;
 
            try {
              ({ entity } = await ctx.findCatalogUser({
                entityRef: {
                  kind: 'user', 
                  namespace: BACKSTAGE_NAMESPACE, 
                  name: id
                }
              }));      
            }
            catch (error)  {
              if(error instanceof NotFoundError){
                entity = {
                  kind: 'user',
                  namespace: BACKSTAGE_NAMESPACE,
                  name: id,
                };
              }
            }

            // Set default group ownerships
            const membershipRefs = entity.relations
              ?.filter(
                (r:any) => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),
              )
              .map((r:any) => r.targetRef) ?? [];

            const ownershipRefs:string[] = Array.from(new Set([`group:${BACKSTAGE_NAMESPACE}/default`, ...membershipRefs]));

            return ctx.issueToken({
              claims: {
                sub: stringifyEntityRef(entity),
                ent: ownershipRefs,
              },
            });
          },
        },
      }),

The above resolver adds default memberships as below:

  1. If authenticated:
    • group:/backstage/default
    • Explicit groups set for the user entity, created in Backstage
  2. If not authenticated (Guest): user:/default/guest

Our approach here is to manage the AuthZ from Backstage by creating the User/Group entities in Backstage and mapping the authenticated user to inherit these specified groups. To achieve this, there should be an identical id (which the user uses to authenticate) and the one created in Backstage. In the above example, this id is populated based on the user’s email, by using the part preceding @ of the email.

Thus we have users.yaml and groups.yaml which creates the users and groups at Backstage end, and maps groups with the user, like below:

apiVersion: backstage.io/v1alpha1
kind: User
metadata:
  namespace: backstage
  name: username.surname
spec:
  profile:
    displayName: Username Surname
  memberOf:
    - admin
    ...
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
   namespace: backstage
   name: admin
spec:
   type: team
   profile:
      displayName: Admin
   children: []

Now that we have the Users and Groups ready with correct memberships defined, the next thing is to setup the permission policy.

As stated, we use a custom permission rule to provided access to Templates based on entity tags. (ABAC).

// permission-rule.ts snippet
// Custom permission rule to authorize based on entity tags
export const isGroupInTagRule = createCatalogPermissionRule({
  name: 'IS_GROUP_IN_TAG',
  description: 'Checks if an entity tag contains an user group, to allow access to the entity',
  resourceType: 'catalog-entity',
  apply: (resource: Entity, claims:string[]) => {
    if (!resource.metadata.tags) {
     return false;
    }
    return resource.metadata.tags
      .some(tag => claims.includes(`group:backstage/${tag.split(':')[1]}`))
  },
  toQuery: (claims:string[]) => ({
    key: 'metadata.tags',
    values: claims.map(group => `group:${group.split('/')[1]}`),
  }),
});

export const isGroupInTag = createConditionFactory(isGroupInTagRule);

You can refer the detailed process to define custom permission rules here, which will be required with the above change to make this work.

So the above permission rule now provides us with a isGroupInTagRule function that can be used in the permission policy to authorize entities based on tags.

Finally let’s have a look at the permission policy:

// permission.ts defining custom authZ policy
class CustomPermissionPolicy implements PermissionPolicy {
  async handle(request: PolicyQuery, user: BackstageIdentityResponse): Promise<PolicyDecision> {

    // Exempt admin from permission checks
    if (isAdmin(user)) {
      return { result: AuthorizeResult.ALLOW };
    }

    // RO permissions
    if (isPermission(request.permission, catalogEntityReadPermission)) {
      return createCatalogConditionalDecision(request.permission, {
        anyOf: [
          catalogConditions.isEntityKind([
            'Domain',
            'Component',
            'System',
            'API',
            'Group',
            'User',
            'Resource',
            'Location',
          ]),
          { // Template RO permission only to groups specified in tags
            allOf: [
              catalogConditions.isEntityKind(['Template']),
              isGroupInTag(
	            user?.identity.ownershipEntityRefs ?? [],
	          ),
            ],
          },
        ],
      });
    }

    // Deny explicit creat/delete permissions
    if (isPermission(request.permission, catalogEntityDeletePermission) || 
        isPermission(request.permission, catalogEntityCreatePermission)) {
      return { result: AuthorizeResult.DENY };
    }

    return { result: AuthorizeResult.ALLOW };
  }
}

// Function to check if user has admin group membership
const isAdmin = (user: BackstageIdentityResponse):boolean => {
  if (typeof(user) === 'object') {
    return user.identity.ownershipEntityRefs.includes('group:backstage/admin');
  }
  return false;
}

The above policy:

  1. Allows full access to users with admin group membership
  2. RO access to all entities (except Templates)
  3. Read access to Templates to users with membership to groups specified in the Template tag

Here is how the Template definition snippet looks, which provides access to users only with the staff membership:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golden-path-template
  title: Golden Path Template
  description: Create a new microservice from scratch following the golden path
  tags:
    - recommended
    - microservice
    - "group:staff"
...

That’s all! This works perfectly, is scalable and achieves its purpose well. 🙂

Design a site like this with WordPress.com
Get started