Configure Backstage
The Portal Controller writes entities to a repository. Backstage has to be told to read them, and needs the SKE plugins installed so that submitting a generated Template does something.
This page covers the Backstage side. Configure the portal itself first, see Backstage.
Prerequisites
-
Backstage app running v1.40 or above: The SKE plugins are tested up to v1.50.4. If you do not have a Backstage app, refer to the Backstage official documentation for instructions on how to create a Backstage app.
-
Node.js 22+: Starting from Backstage v1.50, Node.js 22 or higher is required due to native module dependencies. Ensure your build environment and Docker images use Node 22+.
-
NODE_OPTIONSenvironment variable: Set the following when starting your Backstage app:export NODE_OPTIONS=--no-node-snapshot
Accessing the private npm registry
The SKE plugins are distributed through a private npm registry. Please follow the steps below to enable access to it.
- For local development
- For Docker
To access the private npm registry on your local machine, you will need npm config in a local ~/.npmrc file in your home directory. Create this file if it doesn't already exist,
and add the following content:
# replace <YOUR_TOKEN_HERE> with the provided Syntasso registry token
@syntasso:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken="<YOUR_TOKEN_HERE>"
always-auth=true
For Yarn 3+, use the following command to set a local .yarnrc.yml file:
yarn config set npmScopes --json '{"syntasso": {"npmAlwaysAuth": true, "npmAuthToken": "<YOUR TOKEN HERE>", "npmRegistryServer": "https://npm.pkg.github.com"}}'
For Docker builds, you must update the packages/backend/Dockerfile in your Backstage app so that it can access the private npm registry.
Update the RUN command that is running the yarn install to mount a secret, as described below:
RUN
\
yarn install --frozen-lockfile --production --network-timeout 300000
When running yarn build-image you now need to pass in the additional args --secret id=npmrc,src=$HOME/.npmrc to provide the npm credentials to Docker. Alternatively, you can
update the build-image script in the package.json file to include the additional required flag.
Make sure that the ~/.npmrc file is in the home directory of the user that is running the Docker build. Create it by following the For local development tab above.
Install the plugins
Two packages, one for each half of Backstage:
yarn add @syntasso/plugin-ske-backend --cwd packages/backend
yarn add @syntasso/plugin-ske-frontend @backstage/plugin-permission-react --cwd packages/app
v0.21.1 and later)From @syntasso/plugin-ske-frontend v0.21.1, the packages the frontend plugin
shares with the host app (@backstage/plugin-catalog, @backstage/plugin-catalog-graph,
@backstage/plugin-catalog-react, @backstage/plugin-scaffolder,
@backstage/plugin-scaffolder-react and @backstage/plugin-permission-react) are
declared as peer dependencies, so your app must provide them. Most already ship
with a standard Backstage app; the one that usually does not is
@backstage/plugin-permission-react, which is why it is added explicitly above.
Yarn does not fail the install over a missing peer — it prints a warning
(YN0002: … doesn't provide @backstage/plugin-permission-react) and yarn install
still completes, so it is easy to miss until the app fails to build against the absent
package.
@syntasso/plugin-ske-backend must be at least v0.21.0, and @syntasso/plugin-ske-frontend at
least v0.18.0. Earlier backend versions do not provide the action that
pull-request mode uses to record a request, so a submission opens the pull
request and then fails.
If you are using Spotify Portal for Backstage, publish the plugins to your cloud instance before completing the install — see the Spotify Portal installation guide.
Backend
Add the plugin in packages/backend/src/index.ts:
// SKE plugin
backend.add(import('@syntasso/plugin-ske-backend'));
That single line registers the scaffolder actions the generated Templates use, the routes the frontend calls, and the catalogue provider that surfaces pending requests.
The plugin creates one database table and runs its migrations at startup. If you run more than one Backstage replica, use PostgreSQL rather than SQLite.
Frontend
If your Backstage app uses the new frontend system (default since Backstage v1.49.0), the plugin is automatically discovered — no
changes to EntityPage.tsx are needed. This is also the case if you are using Spotify Portal for Backstage.
The plugin registers the following extensions via its alpha entry point:
- Promise Overview — displays the Overview tab for Promise entities
- Resource Overview — displays the Overview tab for Resource entities
- Resource Manage — displays the Manage tab for Resource entities
To verify the plugin is loaded, check your Backstage app configuration. The plugin should appear under @syntasso/plugin-ske-frontend/alpha in the resolved plugin list.
In packages/app/src/components/catalog/EntityPage.tsx, import the entity pages:
import {
KratixPromiseEntityPage,
KratixResourceEntityPage,
isKratixType,
} from '@syntasso/plugin-ske-frontend';
Then add two cases to the existing componentPage switch, before the default case:
<EntitySwitch.Case if={isKratixType('promise')}>
<KratixPromiseEntityPage>{entityWarningContent}</KratixPromiseEntityPage>
</EntitySwitch.Case>
<EntitySwitch.Case if={isKratixType('resource')}>
<KratixResourceEntityPage>
{entityWarningContent}
</KratixResourceEntityPage>
</EntitySwitch.Case>
The matcher selects Promise and Resource Components using either the kratix.io/type
annotation or spec.type. For example, a Resource can use kratix.io/type: resource or
spec.type: kratix-resource.
Add kratix.io/type: promise to every Promise Component. Although the route matcher
recognises spec.type: kratix-promise, the page also needs the annotation when it loads
Promise data. Without it, the Promise Overview does not load. It shows a
missing-annotation message for Resource fields instead of the Promise details.
This gives Promise entities an overview page, and Resource entities an overview page plus a Manage tab for updating and deleting the resource. For the full list of entity pages, components, annotations, and behaviours, see the SKE Frontend Plugin reference.
Configuring the backend plugin
The plugin can read from and write to your Kratix platform in three ways: directly via the Kubernetes API, indirectly via a Git repository (GitOps mode), or a combination of the two that reads via the Kubernetes API and writes via a Git repository (Hybrid mode). Follow the decision guidance below to decide on the approach most fitting for your platform.
ske.mode decides how an approved request reaches the platform:
| Mode | Reads from | Writes via |
|---|---|---|
kubernetes (default) | Kubernetes API | Kubernetes API, as the signed-in user |
gitops | Git | Git |
hybrid | Kubernetes API | Git |
This is backend-wide and independent of a portal's delivery mode. The same generated Template works under all three.
If you use pull-request delivery mode, you can tune how often Backstage re-checks which requests are still awaiting approval:
ske:
pendingRequests:
refreshSeconds: 60 # default
Configure Backend Access
Choose the mode that best fits your environment and operational requirements:
| Kubernetes API | GitOps | Hybrid | |
|---|---|---|---|
| Real-time status updates | ✓ | ✗ | ✓ |
| Full resource management (kubectl-compatible) | ✓ | ✗ | ✓ |
| Requires direct network access to the platform cluster | ✓ | ✗ | ✓ |
| Routes all changes through a Git repository | ✗ | ✓ | ✓ |
Choose Kubernetes API mode if Backstage can reach the platform cluster over your network. This provides the most complete feature set, including real-time status updates and full resource management.
Choose GitOps mode if Backstage cannot access the platform cluster directly, or if your organisation requires all platform changes to flow through a Git repository.
Choose Hybrid mode if Backstage can reach the platform cluster over your network and your organisation requires all platform changes to flow through a Git repository. Reads use the Kubernetes API (real-time status and visibility of resources created outside Backstage), while create, update, and delete operations are written to Git.
- Kubernetes API (Recommended)
- GitOps
- Hybrid
The plugin uses the Backstage Kubernetes integration to talk to the platform cluster. Once you configure it, you should have a kubernetes section in your app-config.yaml file similar to the following:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: <my platform cluster url>
name: kratixPlatform
authProvider: serviceAccount
skipTLSVerify: true
serviceAccountToken: <service-account-token>
- url: <another kubernetes cluster url>
name: someOtherCluster
authProvider: serviceAccount
serviceAccountToken: <service-account-token>
Using OIDC Authentication
If your Backstage instance authenticates users via an OIDC provider, you can configure the backend plugin to forward the user's OIDC token to the platform cluster. This allows Kubernetes RBAC to enforce access control based on the logged-in Backstage user's identity, rather than using a shared service account.
To enable this, set authProvider: oidc and oidcTokenProvider: <your-provider-name> on the cluster in your kubernetes config:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: <my platform cluster url>
name: kratixPlatform
authProvider: oidc
oidcTokenProvider: <your-provider-name> # e.g. keycloak, okta, auth0
skipTLSVerify: true
See the Configuring OIDC Authentication guide for full setup instructions.
You must now tell the backend plugin which of the configured clusters is the platform cluster. For that, add the following section to your app-config.yaml:
ske:
kubernetes:
# make sure this matches the name of the cluster you want to use
platformName: kratixPlatform
If no ske.kubernetes section is present, the backend plugin will assume Backstage itself is running on the Platform cluster and will load the cluster configuration from the local
machine/pod.
To configure Backstage to talk to Kubernetes via a Git repository, add the following section to your app-config.yaml:
ske:
mode: gitops
scm:
token: "my-access-token" # optional; git access token
repoUrl: "https://github.com/my-org/my-repo" # repository url
type: "github" # optional; git provider, currently one of "github" or "gitlab" (used to determine username if `username` not set)
username: "my-username" # optional; username for basic auth (default: inferred based on `type`)
path: "catalog" # optional; path within the repository (default: "")
branch: "main" # optional; branch to use (default: "main")
defaultAuthor: # optional; git author details
name: "my-name" # default: "SKE"
email: "my@email.com" # default: ske@backstage.io
The backend plugin uses basic auth to authenticate against the Git provider. If you wish to specify a username/password, or use a Git provider other than GitHub or Gitlab, set the
username field, and set the token field to the password or token. Otherwise, the username will be inferred from the type (e.g. "oauth2" for Gitlab). One of username or
type must be provided.
The token is used by the Backstage plugin to retrieve information about Promises and Resource Requests from the configured Github Repository. If you have configured a Github App
for the Github integration, you can leave this field empty and the plugin will use the Github app's short-lived tokens to authenticate.
The repoUrl is the repository to which Backstage will push the resource requests; you must configure your platform to reconcile on new documents — see Connect the Platform to the Git Repository below.
In hybrid mode, the backend plugin reads resource state directly from the Kubernetes API — giving real-time status and visibility of resources created outside Backstage (for example,
via kubectl) — while all create, update, and delete operations are written to a Git repository. It combines the Kubernetes API and GitOps modes and requires the configuration of
both.
Because reads use the Kubernetes API, first configure the Kubernetes integration:
The plugin uses the Backstage Kubernetes integration to talk to the platform cluster. Once you configure it, you should have a kubernetes section in your app-config.yaml file similar to the following:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: <my platform cluster url>
name: kratixPlatform
authProvider: serviceAccount
skipTLSVerify: true
serviceAccountToken: <service-account-token>
- url: <another kubernetes cluster url>
name: someOtherCluster
authProvider: serviceAccount
serviceAccountToken: <service-account-token>
Using OIDC Authentication
If your Backstage instance authenticates users via an OIDC provider, you can configure the backend plugin to forward the user's OIDC token to the platform cluster. This allows Kubernetes RBAC to enforce access control based on the logged-in Backstage user's identity, rather than using a shared service account.
To enable this, set authProvider: oidc and oidcTokenProvider: <your-provider-name> on the cluster in your kubernetes config:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: <my platform cluster url>
name: kratixPlatform
authProvider: oidc
oidcTokenProvider: <your-provider-name> # e.g. keycloak, okta, auth0
skipTLSVerify: true
See the Configuring OIDC Authentication guide for full setup instructions.
Then enable hybrid mode and configure the Git repository that create, update, and delete operations are written to:
ske:
mode: hybrid
kubernetes:
# must match the name of a cluster configured in the `kubernetes` section above
platformName: kratixPlatform
scm:
token: "my-access-token" # optional; git access token
repoUrl: "https://github.com/my-org/my-repo" # repository url
type: "github" # optional; git provider, currently one of "github" or "gitlab"
username: "my-username" # optional; username for basic auth (default: inferred based on `type`)
path: "catalog" # optional; path within the repository (default: "")
branch: "main" # optional; branch to use (default: "main")
defaultAuthor: # optional; git author details
name: "my-name" # default: "SKE"
email: "my@email.com" # default: ske@backstage.io
The ske.scm fields behave exactly as in GitOps mode. Because writes go to Git, the platform must also reconcile the repository back into the cluster — see Connect the Platform to
the Git Repository below.
Connect the Platform to the Git Repository
This step is required for GitOps and Hybrid modes, which write resource requests to a Git repository. It is not needed for Kubernetes API mode.
Your platform needs to watch the Git repository and reconcile any documents that Backstage pushes to it. The steps below cover two common GitOps tools — choose the one your platform uses.
- Flux
- Argo CD
Create a GitRepository source pointing at the same repository you configured in ske.scm:
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: backstage-resources
namespace: flux-system
spec:
interval: 10s
url: https://github.com/my-org/my-repo # must match ske.scm.repoUrl
ref:
branch: main # must match ske.scm.branch
Then create a Kustomization to reconcile documents from that source:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: backstage-resources
namespace: flux-system
spec:
interval: 10s
sourceRef:
kind: GitRepository
name: backstage-resources
path: ./catalog # must match ske.scm.path
prune: true
Apply both resources to your platform cluster:
kubectl apply -f gitrepository.yaml -f kustomization.yaml
For private repositories, you will need to create a Flux secret with your Git credentials and reference it in the GitRepository via spec.secretRef. See the Flux documentation for details.
Create an Application resource that points to the same repository and path you configured in ske.scm:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backstage-resources
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/my-repo # must match ske.scm.repoUrl
targetRevision: main # must match ske.scm.branch
path: catalog # must match ske.scm.path
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
Apply this to your platform cluster:
kubectl apply -f application.yaml
Argo CD will now watch the repository and apply any new documents Backstage pushes to it.
For private repositories, configure Argo CD repository credentials first. See the Argo CD repository docs for details.
Give Backstage access to GitHub
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
The credential needs:
- Contents: read on the catalogue repository the Portal Controller writes to.
- Pull requests: write on the repositories developers raise requests against, for pull-request mode.
- Contents: write on the requests repository, if you use
ske.mode: gitopsorhybrid.
Backstage never writes the catalogue repository, Kratix does, using the state store's own credential. But Backstage does read it. A token scoped only to the request repositories gives you working submissions and a silently empty catalogue.
Discover the generated entities
The Portal Controller writes Promise entities and Resource entities into two separate trees, so configure one catalogue provider for each:
catalog:
providers:
github:
platformPromiseCatalog:
organization: acme
catalogPath: /gitops/backstage/prod/dependencies/**/catalog-info.yaml
filters:
branch: main
repository: platform-catalog
schedule:
frequency: { minutes: 1 }
timeout: { minutes: 3 }
platformResourceCatalog:
organization: acme
catalogPath: /gitops/backstage/prod/resources/**/catalog-info.yaml
filters:
branch: main
repository: platform-catalog
schedule:
frequency: { minutes: 1 }
timeout: { minutes: 3 }
Fill the paths in from your portal's configuration:
| Placeholder | Comes from |
|---|---|
gitops/backstage/prod | The state store's path, then the Destination's path |
repository | The repository in the state store's url |
branch | The state store's branch |
organization | Your GitHub organisation |
Two narrow providers rather than one glob over the whole prefix, because catalogPath takes a
single glob and a wider one would ingest anything else that lands under it.
The default catalogue rules already permit Component and Template, so no catalog.rules change
is needed.
Verify
Once Backstage has run a catalogue refresh you should see:
- A
Componentfor each Promise bound to the portal, with a matching scaffolder Template on the Create page. - A
Componentfor each Resource request, with Manage available on it.
If entities never appear, check the catalogue repository actually contains
catalog-info.yaml files under the two paths above, and that the GitHub credential can read that
repository.
