Skip to main content

Investigate: Template-Info Schema Harmonisation with Backstage

IMPLEMENTATION RULES: Before implementing this plan, read and follow:

Status: Complete — 10 of 12 items shipped; Q6 deferred; Q11 re-decided (2026-04-16)

Goal: Harmonise template-info.yaml with Backstage's catalog-info.yaml patterns so that our ODP (Open Developer Portal) feels familiar to Backstage users and migration between the two systems is simple. Clean up field redundancy (kill dead fields, clarify overlapping ones). Lay the groundwork for yaml-driven template documentation pages.

Last Updated: 2026-04-16

Supersedes: INVESTIGATE-backstage.md (2026-03-31) — that investigation explored generating Backstage Software Template files from our templates. It was written against the old TEMPLATE_INFO key=value format and is now outdated. The still-valid ideas (Backstage export generation, repo location, discovery mechanism) are carried forward here.


Closing note (2026-04-16)

Ten of twelve items in the Decided section below shipped. Verified against the codebase:

  • summary killed — no template has it, generator doesn't reference it
  • links[] adopted — no website: or docs: top-level fields remain
  • maintainers added — rendered as linked avatars in the TemplateHeader card
  • prerequisites added — rendered in the Getting Started card
  • abstract kept and rendered inside the TemplateHeader card (moved there in PR #66)
  • quickstart.setup + quickstart.run split — used across every template
  • Field requirements enforced in validate-metadata.sh

Two items did not ship and are resolved here:

  • Q6 (Backstage export generation) — this was always flagged as "separate investigation for later" (see Q6 below). Remains deferred; no stub filed. When the need surfaces, a fresh investigation should be opened against the current schema (which is now stable post-10-items).
  • Q11 (readme becomes optional) — decided at the time, never shipped. Re-decided today: readme stays required. Every current template has a README and no concrete use case emerged for omitting it. Per the project's general principle ("don't add features for hypothetical future requirements"), the feature flag waited for a real use case that didn't materialize. If one appears later, flip the flag in validate-metadata.sh's MANDATORY_FIELDS at that point.

Background

ODP — Open Developer Portal

The TMP/UIS/DCT three-project architecture is an Open Developer Portal (ODP) — achieving similar functionality to Backstage without requiring Backstage running in the cluster.

ConcernBackstageODP (our system)
RuntimeBackstage backend + frontend (Node.js server)No server — static site (Docusaurus) + CLI tools (DCT) + infra automation (UIS)
Template catalogBackstage Software Catalog UIDocusaurus template pages (auto-generated from template-info.yaml)
Template executionBackstage Scaffolder (backend actions)dev-template CLI (DCT) + uis CLI
Service catalogBackstage entity pagesDocusaurus docs + Environment card
DocumentationTechDocs (mkdocs rendered inside Backstage)Docusaurus pages (auto-generated from template-info.yaml)
Entity descriptorcatalog-info.yamltemplate-info.yaml

Design principle: align with Backstage's concepts and schema so that:

  1. People who know Backstage can easily understand our system
  2. Moving from ODP to Backstage (or vice versa) is simple — field names map cleanly
  3. A Backstage generator can consume the same template-info.yaml data

How Backstage structures entity metadata

Backstage uses a Kubernetes-style envelope (apiVersion/kind/metadata/spec) with exactly three text fields:

Backstage fieldPurposeConstraints
metadata.nameMachine identifier[a-z0-9A-Z][-_.], 1–63 chars, used in URLs/entity references
metadata.titleHuman-friendly display nameOptional, short, UI only — never used in references
metadata.descriptionShort informative overviewThe ONLY description field. "Detailed explanations belong elsewhere."

No "abstract", "summary", or "long description" field exists. Detailed documentation lives in TechDocs, linked via backstage.io/techdocs-ref annotation. The entity descriptor stays lean.

Other Backstage patterns:

  • Annotations (backstage.io/techdocs-ref, github.com/project-slug) — namespaced key-value pairs for linking to external systems
  • Labels — key-value classification pairs for machine queries/filtering
  • Tags — single-value strings for human classification/search
  • Relations are derived, not declared — write spec.dependsOn, system derives the reverse dependencyOf
  • spec.type — what kind of component (service, website, library)
  • spec.lifecycle — what stage (experimental, production, deprecated)

Current State: Our Text Fields

Field audit

FieldLengthDCT usageDocusaurus usageBackstage equivalent
idShortTemplate identification, direct selectionURL slug, page routingmetadata.name
nameShortMenu item label, confirmation dialog titleHeader, sidebar, page titlemetadata.title
descriptionOne-linerMenu item help textHeader subtitle, MDX meta, category index tablemetadata.description
abstract2–3 sentencesConfirmation dialog "About:", post-selection displayCategory index cards (<TemplateCard>) — not on the detail page(no equivalent)
summaryParagraphNot usedNot used(no equivalent)
tagsArrayNot usedTag chips, filteringmetadata.tags
websiteURLNot usedHeader linkmetadata.links[]
docsURLNot usedHeader linkmetadata.links[]

DCT field consumption (verified from source)

The DCT dev-template.sh command fetches template-registry.json and renders a dialog TUI:

  • Category menu: categories[].emoji + categories[].name
  • Template menu: .name as label, .description as help text
  • Confirmation dialog: .name (title), .category, .description, .abstract (as "About:"), .tools, .install_type
  • Post-selection: .name, .abstract

Fields NOT read by DCT: summary, version, tags, logo, website, docs, related, params


Decisions

Decided

  • Kill summary — dead everywhere (DCT never reads it, Docusaurus never renders it). Remove from all 10 template-info.yaml files, generate-registry.ts, and validate-metadata.sh.
  • Q1: Keep abstract — serves a real DCT UI need. Map to helpers.no/abstract annotation when exporting to Backstage.
  • Q2: Skip spec.type and spec.lifecycle — no consumer today; derive at Backstage export time. Add lifecycle only when first deprecated template exists.
  • Q3: Adopt Backstage links[] pattern — replace website + docs with a links[] array.
  • Q4: Skip annotations — closed system, no consumer. Synthesize at Backstage export time.
  • Q5: Moderate yaml-driven pages — render Quick Start from yaml (already has the data, diagrams depend on it). Add prerequisites field. README becomes optional deep-dive.
  • Q7: Add maintainers field — list of GitHub usernames. Rendered on Docusaurus page as linked avatars via github.com/<user>.png. Start with plain strings, assume GitHub. Extend to {id, provider} objects when the first non-GitHub user shows up.
  • Q6: Backstage export — separate investigation for later.
  • Q8: Split quickstart.commands — rename to setup, remove the run command from it. run field holds it separately. No redundancy.
  • Q9: Field requirementsmaintainers required, prerequisites required (every template needs at least "DCT devcontainer running"), links required (at least source code link).
  • Q10: Render abstract on detail page — between header and environment card.
  • Q11: readme stays required (re-decided 2026-04-16 — see Closing note above) — originally decided as "becomes optional, schema cleanup ships first" but never shipped because no concrete use case emerged. Every current template has a README. Keeping the mandatory requirement avoids legislating for a hypothetical future need.

Questions to Answer

Q1. Should abstract be renamed for Backstage alignment?

Backstage has no abstract field. Our abstract is used by DCT as an "About:" block and by Docusaurus on category index cards. Options:

Option A: Keep abstract as-is

Pros: no migration needed, DCT already reads it. Cons: no Backstage equivalent; Backstage users won't expect this field.

Option B: Fold abstract into description

Make description the 2–3 sentence field (matching what DCT shows as "About:"). Use the first sentence for one-liner contexts (menu help text, meta).

Pros: matches Backstage's single-description model; one fewer field. Cons: DCT uses description as a short one-liner and abstract as a longer overview in different UI contexts. Merging them loses the distinction.

Option C: Map abstract to a Backstage annotation

Keep abstract in our yaml, but when exporting to Backstage, map it to an annotation (e.g., helpers.no/abstract). This preserves our two-tier model while staying Backstage-compatible.

Pros: no migration; Backstage export works; ODP keeps its richer model. Cons: Backstage won't render the annotation natively — it's just metadata.

Decision: Option A — keep abstract. It serves a real DCT UI need (the "About:" block in the confirmation dialog). When exporting to Backstage, map it to a helpers.no/abstract annotation. Decided 2026-04-12.

Q2. Should we adopt Backstage's spec.type and spec.lifecycle?

Our install_type (app | stack | overlay) maps loosely to Backstage's spec.type (service | website | library). We don't have a lifecycle field.

Our install_typeBackstage spec.type
appservice or website (depends on the template)
stackresource (infrastructure)
overlayNo clean equivalent — overlays modify existing projects

Decision: Skip. Keep install_type as our routing field. No new type or lifecycle fields — they would have no consumer today (same trap as summary). The Backstage export generator can derive spec.type from install_type + category at export time. Add lifecycle only when we have our first deprecated template and actually need the distinction. Decided 2026-04-12.

Currently we have website and docs as separate top-level fields. Backstage uses a flexible links[] array:

# Backstage style
links:
- url: https://github.com/helpers-no/dev-templates/...
title: Source code
icon: github
- url: https://python.org
title: Python website
icon: web

Should we replace website + docs with a links[] array? This is more extensible (can add arbitrary links without schema changes) and matches Backstage directly.

Decision: Adopt Backstage's links[] pattern. Replace website + docs with a links[] array. Each entry has url, optional title, optional icon, optional type. Decided 2026-04-12.

Migration: the 10 template-info.yaml files change from:

website: "https://python.org"
docs: https://github.com/helpers-no/dev-templates/tree/main/templates/...

to:

links:
- url: "https://python.org"
title: Python website
- url: https://github.com/helpers-no/dev-templates/tree/main/templates/...
title: Source code
icon: github

Impact: generate-registry.ts field reading, <TemplateHeader> component (currently takes website + docs as separate props), generate-docs-markdown.sh (emits those props), DCT (does not read these fields — zero impact).

Q4. Should we adopt Backstage's annotation pattern?

Backstage uses namespaced annotations for external references:

annotations:
backstage.io/techdocs-ref: dir:.
github.com/project-slug: helpers-no/dev-templates

We could use this for:

  • helpers.no/dct-docs → link to DCT tool page
  • helpers.no/uis-docs → link to UIS service page
  • helpers.no/source-template → which template this was created from

Or is this over-engineering for our current scale?

Decision: Skip. Backstage needs annotations because it's a generic plugin platform. We're a closed system where TMP/UIS/DCT know about each other directly — external references are already resolved at build time by generate-registry.ts (e.g., resolvedTools[].docsUrl, resolvedServices[].docsUrl). Synthesize annotations at Backstage export time if needed. Decided 2026-04-12.

Q5. Yaml-driven template pages — how far should we go?

Current page sections and their data sources:

SectionSource todayCould come from yaml?
Headertemplate-info.yaml ✅Already does
Environment cardtemplate-info.yaml ✅Already does
Architecture diagramsregistry (new) ✅Already does
Quick StartREADMEquickstart block has the data
PrerequisitesREADME✅ New field needed
Project StructureREADME🤔 Could auto-scan, but annotations need prose
Development tipsREADME🤔 Language-specific, hard to structure
CI/CD descriptionREADME✅ Could generate from workflow + manifest presence
Related Templatestemplate-info.yaml ✅Already does

How far should we push structured yaml fields vs README prose?

Decision: Moderate. Move Quick Start and Prerequisites to yaml as the rendered source on the Docusaurus page. The quickstart block already has commands, run, title, and note — these fields are also consumed by the architecture Mermaid diagrams (the sequence builder reads quickstart.run for the "developer runs the app" step), so they must be structured and correct. Rendering them directly from yaml eliminates the duplication with the hand-written README Quick Start section and ensures the diagram labels match what the page shows.

README shrinks to optional deep-dive sections (Development tips, Project Structure) that are hard to structure as yaml. Decided 2026-04-12.

New yaml fields needed:

  • prerequisites: string[] — optional list of things that must be in place before running the template (e.g., "UIS provision-host running", "Local Kubernetes cluster"). Rendered as a checklist on the page.
  • quickstart block already exists — just needs to become the rendered source instead of the README duplicate.

Additional decisions from gap analysis (2026-04-12):

  • quickstart.commands cleanup: Remove the run command from commands so it only contains setup steps. The run field holds the run command separately. No more redundancy. Schema becomes setup: string[] (rename from commands) + run: string. All 9 templates need migration (remove the last command from commands). DCT does not read quickstart fields — zero impact.
  • Required vs optional for new fields: maintainers = required (every template needs a contact). prerequisites = optional (E2 templates without services may have none). links = required (every template has at least a source code link).
  • Render abstract on the detail page: Currently only shown on category index cards. Must also render on the template detail page — natural placement between the header and the environment card. Not a schema change, but a rendering change included in this plan.
  • readme becomes optional free text: The README is now supplementary — the template developer decides what goes there (advanced usage, design rationale, anything). The page works fully without it. All structured sections (header, abstract, environment, architecture, quickstart, prerequisites) render from yaml. The readme field in template-info.yaml becomes optional.

Q6. Backstage export generation — still a goal?

The original INVESTIGATE-backstage.md recommended generating backstage/ files (template.yaml + skeleton/catalog-info.yaml) from our templates. Still-valid decisions from that investigation:

  • Repo location: backstage/ inside dev-templates
  • Generator tool: TypeScript (aligns with current pipeline)
  • Sync mechanism: GitHub Actions re-runs on push
  • Discovery: backstage/all-templates.yaml Location entity
  • app-config.yaml entry: single URL pointing to the all-templates file

Questions that need re-answering against the current schema:

  • Updated field mapping (template-info.yaml → Backstage yaml)
  • How does template-registry.json fit? (Could be the data source instead of reading yaml directly)
  • Nunjucks placeholders (${{ }}) in skeleton files — handled cleanly by TypeScript template literals
  • Stack and overlay archetypes — what Backstage entity kind do they map to?

Decision: Separate investigation for later. The schema cleanup (kill summary, links[], prerequisites, maintainers, render quickstart) ships first — it has immediate value and no dependency on the export work. Decided 2026-04-12.


Impact of Killing summary

FileChange needed
All 10 template-info.yaml filesRemove summary: field
scripts/generate-registry.ts (~line 617)Remove summary: raw.summary?.trim()
scripts/generate-registry.ts (~line 530)Remove if (!tmpl.summary) fail(...) validation
scripts/validate-metadata.shRemove any summary-specific check (if separate from generate-registry.ts)
scripts/generate-docs-markdown.sh (~line 91)Remove summary=$(jq -r ".templates[$i].summary" "$REGISTRY") (read but never used)
website/src/data/template-registry.jsonField disappears on regeneration
DCT scriptsNo change (never read it)
Docusaurus componentsNo change (never rendered it)

Next Steps

  • User answers Q1–Q6
  • Remove summary from all files (decided — can execute immediately)
  • Create a plan based on the answered questions
  • Backstage export generation: decide if separate investigation or folded in here