Stage 7 · Master
Developer Portal & Software Catalog
Scaffolder: Golden Path Templates
Create parameterized templates that generate repos with CI/CD, observability, security policies, and docs pre-configured.
Scaffolder Overview
The Scaffolder is Backstage's template engine. It takes a parameterized template, collects user input, renders the template files, executes actions (create repo, register catalog, configure CI/CD), and outputs links to the created resources. It's the primary interface for golden paths.
Template Structure
golden-path-go-service/
├── template.yaml # Scaffolder template definition
├── skeleton/ # Files to render (Nunjucks templates)
│ ├── .github/
│ │ ├── workflows/
│ │ │ ├── ci.yaml.njk
│ │ │ └── cd.yaml.njk
│ │ └── dependabot.yaml.njk
│ ├── manifests/
│ │ ├── base/
│ │ │ ├── deployment.yaml.njk
│ │ │ ├── service.yaml.njk
│ │ │ ├── servicemonitor.yaml.njk
│ │ │ └── kustomization.yaml.njk
│ │ └── overlays/
│ │ ├── preview/
│ │ │ └── kustomization.yaml.njk
│ │ └── production/
│ │ └── kustomization.yaml.njk
│ ├── Dockerfile.njk
│ ├── devcontainer.json.njk
│ ├── Tiltfile.njk
│ ├── RUNBOOK.md.njk
│ ├── ARCHITECTURE.md.njk
│ ├── .golangci.yaml.njk
│ ├── go.mod.njk
│ └── main.go.njk
└── .github/
└── workflows/
└── template-test.yaml # CI for template itself
Templates use Nunjucks (.njk) for rendering. The skeleton/ directory contains all files that will be generated. template.yaml defines parameters, steps, and output.
Parameters & Validation
spec:
owner: platform-team
type: service
parameters:
- title: Service Identity
required: [name, owner, team]
properties:
name:
type: string
title: Service Name
description: "Lowercase, hyphenated (e.g., payment-processor)"
pattern: "^[a-z][a-z0-9-]{1,61}[a-z0-9]$"
ui:field: ServiceName
owner:
type: string
title: Owner Email
format: email
team:
type: string
title: Team
enum: [payments, identity, notifications, platform, data]
ui:field: OwnerPicker
description:
type: string
title: Description
language:
type: string
title: Language
const: go
enum: [go]
- title: Platform Capabilities
properties:
capabilities:
type: array
title: Required Capabilities
description: "Platform-managed dependencies"
items:
type: string
enum: [postgresql, redis, kafka, service-mesh, pubsub, blob-storage]
default: []
ui:widget: checkboxes
- title: Repository
required: [repoUrl]
properties:
repoUrl:
type: string
title: Repository URL
description: "GitHub repository (e.g., github.com/org/repo)"
pattern: "^github\.com/[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
Parameters use JSON Schema with Backstage UI extensions (ui:field, ui:widget) for custom widgets.
Steps: Actions & Templates
Steps execute sequentially. Each step has an action (what to do) and input (parameters). Steps can reference outputs from previous steps via ${{ steps.step-id.output.field }}.
steps:
# 1. Fetch and render template skeleton
- id: fetch-template
name: Fetch Template Skeleton
action: fetch:template
input:
url: ./skeleton
values:
service_name: ${{ parameters.name }}
service_owner: ${{ parameters.owner }}
service_team: ${{ parameters.team }}
service_description: ${{ parameters.description }}
capabilities: ${{ parameters.capabilities }}
repo_url: ${{ parameters.repoUrl }}
# 2. Initialize Git repository
- id: init-git
name: Initialize Git Repository
action: shell:git
input:
commands:
- git init
- git add -A
- git commit -m "chore: initial scaffold from golden path template"
dir: ${{ steps.fetch-template.output.dir }}
# 3. Create GitHub repository
- id: create-repo
name: Create GitHub Repository
action: github:create-repo
input:
repoUrl: ${{ parameters.repoUrl }}
description: ${{ parameters.description }}
visibility: private
defaultBranch: main
# 4. Push initial commit
- id: push-repo
name: Push to GitHub
action: shell:git
input:
commands:
- git remote add origin https://github.com/${{ parameters.repoUrl }}.git
- git push -u origin main
dir: ${{ steps.fetch-template.output.dir }}
# 5. Register in software catalog
- id: register-catalog
name: Register in Software Catalog
action: catalog:register
input:
repoUrl: ${{ parameters.repoUrl }}
catalogInfo:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
annotations:
github.com/project-slug: ${{ parameters.repoUrl }}
backstage.io/techdocs-ref: dir:.
labels:
team: ${{ parameters.team }}
platform/capabilities: ${{ join parameters.capabilities "," }}
spec:
type: service
lifecycle: production
owner: ${{ parameters.team }}
system: ${{ parameters.team }}-system
# 6. Configure branch protection
- id: branch-protection
name: Configure Branch Protection
action: github:branch-protection
input:
repoUrl: ${{ parameters.repoUrl }}
branch: main
requiredStatusChecks:
- "ci/lint"
- "ci/test"
- "ci/security-scan"
- "ci/build"
requiredReviews: 1
dismissStaleReviews: true
requireCodeOwnerReviews: true
# 7. Create ArgoCD Application
- id: create-argocd-app
name: Create ArgoCD Application
action: argocd:create-application
input:
name: ${{ parameters.name }}
project: ${{ parameters.team }}
source:
repoURL: https://github.com/${{ parameters.repoUrl }}.git
targetRevision: HEAD
path: manifests/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: ${{ parameters.team }}-${{ parameters.name }}
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
output:
links:
- title: Repository
url: https://github.com/${{ parameters.repoUrl }}
- title: ArgoCD Application
url: https://argocd.platform.example.com/applications/${{ parameters.team }}-${{ parameters.name }}
- title: Grafana Dashboard
url: https://grafana.platform.example.com/d/service-${{ parameters.name }}
- title: Runbook
url: https://github.com/${{ parameters.repoUrl }}/blob/main/RUNBOOK.md
Complete template steps: render → git init → create repo → push → catalog → branch protection → ArgoCD → output links.
Built-in & Custom Actions
| Category | Actions |
|---|---|
| Fetch | fetch:template, fetch:plain, fetch:github, fetch:gitlab |
| Publish | github:create-repo, github:create-pull-request, gitlab:create-repo, bitbucket:create-repo |
| Catalog | catalog:register, catalog:unregister |
| Git | shell:git (init, add, commit, push, tag) |
| CI/CD | github:branch-protection, github:actions-secrets, argocd:create-application, argocd:sync |
| Cloud | aws:cloudformation, gcp:deployment-manager, azure:arm |
| Custom | Write TypeScript actions for org-specific logic |
// packages/backend/src/actions/provision-database.ts
import { createAction } from '@backstage/plugin-scaffolder-node';
export const provisionDatabaseAction = createAction({
id: 'platform:provision-database',
description: 'Provisions a PostgreSQL database via Platform API',
schema: {
input: z.object({
serviceName: z.string(),
team: z.string(),
plan: z.enum(['dev', 'staging', 'prod']).default('dev'),
}),
output: z.object({
connectionString: z.string(),
host: z.string(),
database: z.string(),
}),
},
async handler(ctx) {
const { serviceName, team, plan } = ctx.input;
const response = await fetch(`${process.env.PLATFORM_API}/databases/postgresql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${ctx.auth.token}` },
body: JSON.stringify({ name: serviceName, team, plan }),
});
if (!response.ok) throw new Error(`Failed to provision DB: ${response.statusText}`);
return response.json();
},
});
Custom actions integrate with your Platform API. Register in backend plugin.
Output & Registration
- Output links: Repository, ArgoCD, Grafana, Runbook, API docs — one-click access
- Catalog registration: Automatic entity creation with annotations, labels, ownership
- Branch protection: Required checks, reviews, code owners — enforced from day one
- ArgoCD app: Auto-sync, self-heal, prune — GitOps from first commit
- Secrets: GitHub Actions secrets for platform API tokens, registry credentials
Best Practices
- Template CI: Test templates in CI — render with test params, verify output compiles, passes lint
- Version templates: Semver templates, lockfile in generated repo (
platform-template.lock) - Migration tooling: Codemods for breaking template changes,
platform migrate templateCLI - Documentation: Template README with parameters, capabilities, customization guide
- Feedback loop: Track template adoption, escape hatch usage, time-to-first-deploy
- Security: Scan templates for secrets, validate generated code in CI
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.