# VeeCode Platform Documentation
> Documentation for VeeCode DevPortal, Admin-UI, Platform and VKDR-CLI.
This file contains all documentation content in a single document following the llmstxt.org standard.
## How to Use the API Catalog
Follow this step-by-step guide to effectively utilize the **API Catalog** and streamline your API discovery and integration processes.
---
## **Step-by-Step Instructions**
1. **Access the API Catalog**
Click on the **"Catalog"** tab in the sidebar, then filter by **Kind: API** to display only API entities.
2. **Explore or Search for APIs:**
Browse the available APIs or use the search bar to find specific ones by name, owner, or tag.
3. **Review the Documentation:**
Select an API to open its entity page. Examine the API spec details, including:
- Available endpoints;
- Expected parameters and responses;
- Authentication requirements;
- Code examples and tutorials, if available via TechDocs.
4. **Review API Metadata:**
Check the entity's metadata for ownership, system membership, related components, and lifecycle status. Use this to understand the API's maturity and who to contact for support.
5. **Check Related Entities:**
Use the **Relations** panel to navigate to the system, components, or resources that depend on or provide this API.
6. **Seek Support When Needed:**
If you encounter issues or have questions, contact the API owner listed in the entity page, or reach out to the DevPortal support team.
---
## **Tips for Maximizing Efficiency**
- **Stay Organized:** Bookmark frequently used APIs for quick access.
- **Document Your Integrations:** Keep records of best practices and lessons learned for future reference.
- **Keep Up-to-Date:** Regularly review the catalog for new APIs or updates to existing ones.
- **Register Your APIs:** If your team owns an API, register it in the catalog by adding a `catalog-info.yaml` with `kind: API` and the appropriate `spec.definition`. See the [Catalog concepts page](../catalog.md) for supported spec formats.
---
## API Catalog
The **API Catalog** is the section of the Software Catalog that surfaces API entities — registered API definitions that teams can discover, browse, and document centrally. It enhances resource sharing and simplifies API governance across teams.
---
## Key Features
1. **API Discovery**
- The **API Catalog** offers centralized access to organizational APIs, with search and filter tools to quickly locate specific APIs.
- APIs are categorized by kind, owner, system, and tags for easy navigation.
2. **Comprehensive Documentation**
- Detailed documentation is rendered from the API spec file for each API entity, including endpoints, parameters, response formats, and authentication requirements.
- Additionally, code samples and tutorials may be available if included in linked TechDocs.
3. **Multiple Spec Formats**
- The catalog supports **OpenAPI**, **AsyncAPI**, **GraphQL**, and **gRPC** spec formats.
- You can register multiple API entities (one per version) if you need to maintain multiple versions in the catalog simultaneously. The platform does not manage versioning automatically.
4. **Documentation-Only Viewing**
- The API catalog is a documentation and discovery layer. It renders spec content for reference — it does not provide a built-in sandbox or live "try it out" execution environment.
- To test API endpoints, use your external tooling (Postman, curl, etc.) against the actual service.
5. **Access Control via RBAC**
- Catalog RBAC controls who can *view* API entities in the portal. Assign the `catalog.entity.read` permission to the appropriate roles.
- The portal does not act as an API gateway and does not enforce authentication on the APIs themselves.
---
## Why Use the API Catalog?
- **Promotes Reuse**: Developers can discover existing APIs, reducing duplication and ensuring consistency across applications.
- **Facilitates Collaboration**: Shared documentation lets teams access and understand APIs without contacting the owning team directly.
- **Increases Productivity**: Spec rendering and search capabilities enable faster onboarding and integration.
- **Improves Governance**: Centralized registration makes it easy to audit which APIs exist, who owns them, and what state they are in.
---
## Available Actions
Actions are functions registered in the scaffolder backend. You invoke them by `id` inside a `step`. This page lists everything pre-registered in VeeCode DevPortal — no extra installation needed.
To see all currently loaded actions in a running instance, open `/create/actions` in your DevPortal URL.
---
## Quick lookup
| Action ID | What it does | Source module |
|---|---|---|
| `fetch:template` | Copies a skeleton directory, rendering Nunjucks variables into file contents and paths | `@backstage/plugin-scaffolder-backend` |
| `fetch:plain` | Downloads a directory from a URL without any templating | `@backstage/plugin-scaffolder-backend` |
| `fetch:plain:file` | Downloads a single file from a URL | `@backstage/plugin-scaffolder-backend` |
| `catalog:register` | Registers a `catalog-info.yaml` as a new entity | `@backstage/plugin-scaffolder-backend` |
| `catalog:fetch` | Fetches an existing entity from the catalog | `@backstage/plugin-scaffolder-backend` |
| `debug:log` | Writes a message or lists workspace files in the step log | `@backstage/plugin-scaffolder-backend` |
| `publish:github` | Creates a GitHub repo and pushes workspace content | `@backstage/plugin-scaffolder-backend-module-github` |
| `github:repo:create` | Creates a GitHub repo without pushing content | `@backstage/plugin-scaffolder-backend-module-github` |
| `github:issues:create` | Creates a GitHub issue | `@backstage/plugin-scaffolder-backend-module-github` |
| `publish:gitlab` | Creates a GitLab project and pushes workspace content | `@backstage/plugin-scaffolder-backend-module-gitlab` |
| `publish:azure` | Creates an Azure DevOps repo and pushes workspace content | `@backstage/plugin-scaffolder-backend-module-azure` |
| `notification:send` | Sends a Backstage notification to entities or as broadcast | `@backstage/plugin-scaffolder-backend-module-notifications` |
| `http:backstage:request` | Makes an authenticated HTTP request to any Backstage API | `@roadiehq/scaffolder-backend-module-http-request` |
| `roadiehq:utils:fs:write` | Writes a string to a file in the workspace | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:fs:append` | Appends content to a file (creates if absent) | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:fs:parse` | Parses a JSON or YAML file into an object | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:serialize:json` | Serializes data to a JSON string | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:serialize:yaml` | Serializes data to a YAML string | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:merge` | Deep-merges content into an existing YAML or JSON file | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:json:merge` | Deep-merges JSON into an existing JSON file | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:utils:sleep` | Waits N seconds before continuing | `@roadiehq/scaffolder-backend-module-utils` |
| `roadiehq:aws:s3:cp` | Uploads workspace content to an S3 bucket | `@roadiehq/scaffolder-backend-module-aws` |
| `roadiehq:aws:ecr:create` | Creates an ECR container image repository | `@roadiehq/scaffolder-backend-module-aws` |
| `roadiehq:aws:secrets-manager:create` | Creates a secret in AWS Secrets Manager | `@roadiehq/scaffolder-backend-module-aws` |
| `argocd:create-resources` | Creates an Argo CD project and application | `@roadiehq/scaffolder-backend-argocd` |
| `veecode:kong:deck:ping` | Pings a Kong Gateway instance to verify connectivity | `@veecode-platform/plugin-scaffolder-backend-module-kong` |
| `veecode:kong:deck:generate` | Converts an OpenAPI spec to a Kong declarative config using decK | `@veecode-platform/plugin-scaffolder-backend-module-kong` |
| `veecode:kong:deck:sync` | Syncs a Kong declarative config to a running Kong Gateway | `@veecode-platform/plugin-scaffolder-backend-module-kong` |
Source: [Backstage built-in actions](https://backstage.io/docs/features/software-templates/builtin-actions) · [Roadie scaffolder actions](https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/scaffolder-actions) · [VeeCode OpenAPI template](https://github.com/veecode-platform/devportal-base/blob/main/examples/template-openapi/template.yaml)
---
## Fetch actions
### `fetch:template`
Downloads a directory, renders all files as Nunjucks templates substituting `${{ values.* }}` variables, and places the result in the workspace. This is the action you use to scaffold files from a skeleton.
```yaml
- id: fetch-base
name: Fetch skeleton
action: fetch:template
input:
url: ./content # relative path inside the template repo, or any HTTPS URL
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
```
Variables passed via `values` are accessible inside skeleton files as `${{ values.name }}`.
Source: [Backstage API](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-backend.index.createFetchTemplateAction.html)
### `fetch:plain`
Downloads a directory without any templating — contents are copied as-is.
```yaml
- id: fetch-docs
name: Fetch documentation skeleton
action: fetch:plain
input:
url: https://github.com/my-org/doc-templates/tree/main/mkdocs
targetPath: docs/
```
### `fetch:plain:file`
Downloads a single file.
```yaml
- id: fetch-ci-config
name: Fetch CI config
action: fetch:plain:file
input:
url: https://raw.githubusercontent.com/my-org/ci-configs/main/.github/workflows/ci.yml
targetPath: .github/workflows/ci.yml
```
---
## Debug
### `debug:log`
Writes a message to the step execution log, or lists all files currently in the workspace. Useful while developing and troubleshooting templates.
```yaml
# Log a message
- id: log-name
action: debug:log
input:
message: "Creating service: ${{ parameters.name }}"
# List all files in the workspace
- id: list-workspace
action: debug:log
input:
listWorkspace: true
```
Source: [Backstage API](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-backend.index.createDebugLogAction.html)
---
## Catalog actions
### `catalog:register`
Registers a `catalog-info.yaml` in the catalog. Use after publishing a repo to make the new component appear immediately without manual registration.
```yaml
- id: register
name: Register in catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
```
**Outputs:** `entityRef` (e.g., `component:default/my-service`), `catalogInfoUrl`.
Source: [Backstage API](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-backend.index.createCatalogRegisterAction.html)
### `catalog:fetch`
Fetches an existing entity. Useful when a step needs metadata from the catalog.
```yaml
- id: get-owner
name: Fetch owner entity
action: catalog:fetch
input:
entityRef: group:default/platform-team
```
**Output:** `entity` (the full entity object, accessible as `${{ steps['get-owner'].output.entity.metadata.name }}`).
---
## Publish actions
### `publish:github`
Creates a GitHub repository and pushes the current workspace content.
```yaml
- id: publish
name: Publish to GitHub
action: publish:github
input:
repoUrl: ${{ parameters.repoUrl }}
description: ${{ parameters.name }} service
defaultBranch: main
repoVisibility: private # private | public | internal
```
**Outputs:** `remoteUrl`, `repoContentsUrl`, `commitHash`.
Source: [Backstage API](https://backstage.io/api/next/functions/_backstage_plugin-scaffolder-backend-module-github.createPublishGithubAction.html)
### `github:repo:create`
Creates a GitHub repository without pushing any content. Use when you want to create the repo in one step and push content in a separate step.
```yaml
- id: create-repo
name: Create GitHub repo
action: github:repo:create
input:
repoUrl: ${{ parameters.repoUrl }}
description: ${{ parameters.name }} service
repoVisibility: private
```
**Outputs:** `remoteUrl`, `repoContentsUrl`.
### `github:issues:create`
Creates a GitHub issue in an existing repository.
```yaml
- id: create-issue
name: Create onboarding issue
action: github:issues:create
input:
repoUrl: ${{ parameters.repoUrl }}
title: "Onboarding checklist for ${{ parameters.name }}"
body: "Follow these steps to complete your service setup."
assignees:
- ${{ parameters.owner }}
```
**Outputs:** `issueUrl`, `issueNumber`.
### `publish:gitlab`
Creates a GitLab project and pushes workspace content.
```yaml
- id: publish
name: Publish to GitLab
action: publish:gitlab
input:
repoUrl: ${{ parameters.repoUrl }}
description: ${{ parameters.name }} service
defaultBranch: main
repoVisibility: private
```
**Outputs:** `remoteUrl`, `repoContentsUrl`, `commitHash`, `projectId`.
### `publish:azure`
Creates an Azure DevOps repository and pushes workspace content.
```yaml
- id: publish
name: Publish to Azure DevOps
action: publish:azure
input:
repoUrl: ${{ parameters.repoUrl }}
description: ${{ parameters.name }} service
defaultBranch: main
```
**Outputs:** `remoteUrl`, `repoContentsUrl`, `repositoryId`, `commitHash`.
---
## Notification action
### `notification:send`
Sends a Backstage notification. Requires the notifications plugin to be enabled.
```yaml
- id: notify
name: Notify team
action: notification:send
input:
recipients: entity
entityRefs:
- ${{ parameters.owner }}
title: Your service is ready
info: "${{ parameters.name }} was created successfully"
severity: normal # low | normal | high | critical
```
Set `recipients: broadcast` to notify all users instead of specific entities.
---
## HTTP request
### `http:backstage:request`
Makes an authenticated request to any Backstage API (catalog, proxy endpoints, custom plugin backends). The authenticated user's token is forwarded automatically.
```yaml
- id: call-api
name: Call internal API
action: http:backstage:request
input:
method: POST
path: /proxy/my-backend-service/api/endpoint
headers:
content-type: application/json
body:
key: ${{ parameters.value }}
```
**Outputs:** `body`, `code`, `headers`.
Source: [Roadie README](https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/scaffolder-actions/scaffolder-backend-module-http-request/README.md)
---
## File utility actions
### Write, append, parse
```yaml
# Write a file
- id: write-config
action: roadiehq:utils:fs:write
input:
path: config/settings.json
content: '{"env": "${{ parameters.environment }}"}'
# Append to a file (creates it if it does not exist)
- id: append-entry
action: roadiehq:utils:fs:append
input:
path: CHANGELOG.md
content: "\n## ${{ parameters.version }}\n"
# Parse a YAML or JSON file into an object for use in later steps
- id: parse-config
action: roadiehq:utils:fs:parse
input:
path: config/values.yaml
parser: yaml # yaml | json | multiyaml
```
**Output of `fs:parse`:** `content` (the parsed object, usable as `${{ steps['parse-config'].output.content.someField }}`).
### Merge
Deep-merges content into an existing YAML or JSON file without overwriting unrelated keys:
```yaml
- id: patch-values
action: roadiehq:utils:merge
input:
path: helm/values.yaml
content:
image:
tag: ${{ parameters.imageTag }}
replicaCount: ${{ parameters.replicas }}
```
### Serialize
Converts a JavaScript object to a YAML or JSON string:
```yaml
- id: to-yaml
action: roadiehq:utils:serialize:yaml
input:
data:
name: ${{ parameters.name }}
replicas: ${{ parameters.replicas }}
```
**Output:** `serialized` (the string).
Source: [Roadie README](https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/scaffolder-actions/scaffolder-backend-module-utils/README.md)
---
## AWS actions
```yaml
# Upload workspace content to S3
- id: upload-artifacts
name: Upload to S3
action: roadiehq:aws:s3:cp
input:
bucket: my-artifacts-bucket
region: us-east-1
# Create an ECR image repository
- id: create-ecr
name: Create ECR repository
action: roadiehq:aws:ecr:create
input:
repoName: ${{ parameters.name }}
region: us-east-1
imageMutability: MUTABLE
# Create a secret in AWS Secrets Manager
- id: create-secret
name: Create secret
action: roadiehq:aws:secrets-manager:create
input:
name: ${{ parameters.name }}/api-key
region: us-east-1
description: API key for ${{ parameters.name }}
```
Source: [Roadie README](https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/scaffolder-actions/scaffolder-backend-module-aws/README.md)
---
## Argo CD
### `argocd:create-resources`
Creates an Argo CD project and application. Requires the Argo CD plugin configured in `app-config.yaml` and an Argo CD user with `create` permissions.
```yaml
- id: create-argocd
name: Create Argo CD resources
action: argocd:create-resources
input:
appName: ${{ parameters.name }}
argoInstance: main # must match an instance name in app-config.yaml
namespace: ${{ parameters.name }}
repoUrl: ${{ steps['publish'].output.remoteUrl }}
labelValue: ${{ parameters.name }}
path: k8s/
```
Source: [Roadie README](https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/scaffolder-actions/scaffolder-backend-argocd/README.md)
---
## Kong (VeeCode)
These actions drive Kong Gateway configuration through the [decK CLI](https://docs.konghq.com/deck/). They require a Kong instance reachable from the DevPortal backend.
```yaml
# 1. Verify connectivity before running further steps
- id: test-kong
name: Test Kong connection
action: veecode:kong:deck:ping
input: {}
# 2. Generate Kong declarative config from an OpenAPI spec
- id: generate-kong-config
name: Generate Kong config
action: veecode:kong:deck:generate
input:
openapiSpec: ${{ parameters.openapiInline }} # OpenAPI YAML or JSON as a string
outputPath: kong.yaml
deckTag: ${{ parameters.name }}
removePathEOLAnchor: true
# 3. Sync the generated config to Kong Gateway
- id: sync-kong
name: Sync to Kong
action: veecode:kong:deck:sync
input:
kongConfigPath: kong.yaml
deckTag: ${{ parameters.name }}
```
Source: [VeeCode OpenAPI template](https://github.com/veecode-platform/devportal-base/blob/main/examples/template-openapi/template.yaml)
---
## References
- [Backstage: Built-in actions](https://backstage.io/docs/features/software-templates/builtin-actions) — official upstream reference
- [Roadie scaffolder actions](https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/scaffolder-actions) — source for `roadiehq:*`, `argocd:*`, `http:backstage:request`
- [VeeCode example templates](https://github.com/veecode-platform/devportal-base/tree/main/examples) — real templates that ship with DevPortal
- [Writing Templates](./writing-templates) — how to use these actions in a template
- [Custom Action](../plugins/development/custom-action) — write your own action in TypeScript when nothing here fits
---
## Catalog
The **Software Catalog** is the central hub of DevPortal — a registry of all software components, APIs, infrastructure resources, systems, groups, and users in your organization. It is built on Backstage's catalog model and covers all entity kinds that Backstage supports.
---
## Why the catalog is foundational
The catalog is not an optional feature — it is the substrate that the rest of the portal binds to.
- **Plugins are context-aware via annotations.** A plugin loaded via a preset or `dynamic-plugins.yaml` adds nothing visible until it finds an entity carrying the right annotation. Without entities in the catalog, plugins have no subject to attach to. (See [Composing a Portal](./portal-composition.md) for the full three-layer model.)
- **Templates produce catalog entries.** Every component a team creates via the scaffolder registers a `catalog-info.yaml`, which is how the portal learns the new service exists.
- **RBAC operates on catalog entities.** Permissions are evaluated against entity ownership and relations — `spec.owner`, `spec.system`, group membership. Without the catalog populated, access control has nothing to enforce.
This means Day-0 work is catalog work: register your services, APIs, and infrastructure before enabling plugins or configuring backends.
---
## **Entity Kinds**
The catalog tracks the following entity kinds (configured in `catalog.rules`):
| Kind | Description |
| --- | --- |
| **Component** | A software component: service, website, library, etc. |
| **API** | An API definition (OpenAPI, AsyncAPI, GraphQL, gRPC) |
| **Resource** | Infrastructure resources: databases, clusters, environments, etc. |
| **System** | A collection of related components and resources |
| **Template** | A scaffolder template for creating new components |
| **Group** | An organizational unit or team |
| **User** | An individual user |
| **Location** | A pointer to external catalog definition files |
All entities are described in `catalog-info.yaml` files and registered via the catalog import flow or auto-discovery.
---
## **API Specification Formats**
When viewing an API entity, the catalog renders its spec for documentation. Supported spec types include:
- **OpenAPI** (REST APIs — Swagger/OpenAPI 2 or 3)
- **AsyncAPI** (event-driven/message-based APIs)
- **GraphQL** (GraphQL schema definitions)
- **gRPC** (Protobuf-based APIs)
Each API entity displays its endpoints, request/response schemas, and authentication details as defined in the spec file. The catalog does not provide a live "try it out" sandbox — it is a documentation and discovery layer.
---
## **Navigating the Catalog**
To access the catalog, click on the **"Catalog"** tab in the navigation bar. You can use filters to refine your search by name, kind, owner, or tags. Once you select an item, a brief description will be displayed.
---
### **Viewing APIs**
Selecting an API from the catalog provides a detailed overview, including:
- Endpoints, request and response parameters (from the spec file).
- Authentication requirements.
- Spec format (OpenAPI, AsyncAPI, gRPC, etc.).
The catalog supports **OpenAPI, AsyncAPI, GraphQL, and gRPC** specification formats. Documentation is always derived from the registered spec file.
### **Viewing Templates**
Selecting a Template entity in the catalog shows the template's description, metadata, and links to the scaffolder wizard. To create a component from it, click **"Choose"** to launch the scaffolder. See [Software Templates](./software-template.md) for details.
### **Viewing Components**
Choosing a component from the catalog displays an overview that includes:
- Its source code repository.
- Any associated plugins configured via catalog annotations.
- Links to related entities (owner, system, dependencies).
### **Viewing Resources**
Resources represent infrastructure entities such as clusters, databases, and environments. DevPortal uses the `Resource` kind for environments and clusters in the [Environment/Cluster journey](./environment-cluster-journey-veecode-platform.md).
---
### **Accessing Documentation**
For components with TechDocs configured, a **"Documentation"** tab is available directly on the entity page, eliminating the need to switch between tools.
---
### **Registering Existing Components**
To add a pre-existing component to the catalog:
1. Click **"Register an Existing Component"** on the Create page.
2. Provide the URL of your repository.
3. Link to an entity file such as `catalog-info.yaml`. The wizard will:
- Analyze the file for entities.
- Add valid entities to the DevPortal catalog.
- Suggest a Pull Request for repositories missing the required configuration.
---
## **Get Started with a Video Tutorial**
---
## Configuration Hierarchy
Backstage's native config system merges multiple `--config` files in the order
they are supplied. The merge is **deep** (object keys are combined) and
**last-wins** on scalar values: if two files set the same leaf key, the later
file's value survives. DevPortal's entrypoint assembles that `--config` chain
at boot from base distribution files, preset-generated files, and your
overrides. Knowing the chain tells you exactly where to put a setting and why it
takes effect — or why it doesn't.
**Quick answer — where do I put my override?**
- Operator customizations (branding, catalog locations, integration credentials): `app-config.local.yaml` (layer 5).
- Auth provider and SCM integration (set once per environment): selected via `VEECODE_PRESETS`, which generates the preset layer (layer 4). See [Presets](./presets.md).
- Plugin-specific backend config (Kubernetes cluster URLs, SonarQube base URL, etc.): also goes in `app-config.local.yaml`, or is injected via `pluginConfig` in `dynamic-plugins.yaml` (layer 6). See [Composing a Portal](./portal-composition.md) for the relationship between plugin loading and backend config.
---
## The precedence chain
Entries are listed lowest to highest priority. A file loaded later wins on any
overlapping key.
| Order | File | When loaded |
| --- | --- | --- |
| 1 | `app-config.yaml` | Base distribution defaults (shipped in the image) |
| 2 | `app-config.production.yaml` | Container / production overrides (shipped in the image) |
| 3 | `app-config.distro.yaml` | VeeCode distro defaults (~10 lines, escape hatch; shipped in the image) |
| 4 | `app-config.preset-.yaml` | One per selected preset, in `VEECODE_PRESETS` order |
| 5 | `app-config.local.yaml` | **Your** operator overrides (volume-mounted `app-config.local.yaml` only) |
| 6 | `dynamic-plugins-root/app-config.dynamic-plugins.yaml` | Generated at boot from each enabled plugin's `pluginConfig:` |
| 7 | `app-config.saas.yaml` | Decoded from `VEECODE_APP_CONFIG` (base64); wins over all earlier layers |
Files 1–3 are always present inside the image. File 4 is emitted once per
preset that declares an `appConfig:` block, in `VEECODE_PRESETS` order. Files
5–7 are conditional — each is skipped if absent.
The source of truth is the `EXTRA_ARGS` construction in `entrypoint.sh`.
:::note This is not the V1 seven-layer profile chain
V2 has no `app-config..yaml` layer. The profile slot (old layer 3) is
replaced by **one file per selected preset** (layer 4 above), assembled in
`VEECODE_PRESETS` order. See [Presets](./presets.md).
:::
---
## Where `app-config.local.yaml` sits
`app-config.local.yaml` is your primary customization target. It loads at
**position 5** — after every preset-generated config, so it **wins over preset
fragments**. You do not need to replicate anything from earlier files; supply
only the keys you want to override or add, and Backstage deep-merges the rest.
```bash
docker run \
-v $(pwd)/app-config.local.yaml:/app/app-config.local.yaml:ro \
veecode/devportal:2.1.3
```
### Example: override a single preset value
Suppose the `github` preset enables the GitHub catalog provider at a 30-minute
refresh frequency and you want 5 minutes. In your `app-config.local.yaml`:
```yaml
catalog:
providers:
github:
default:
schedule:
frequency: { minutes: 5 }
```
Because `app-config.local.yaml` loads after all preset files, your `frequency`
wins. You do not repeat the rest of the provider block — the deep-merge keeps it.
:::note `VEECODE_APP_CONFIG` — config without a file mount
In deployments where you cannot mount a file (ArgoCD managing plain manifests,
a CI-injected environment, or the VeeCode SaaS), encode your operator config as
base64 and pass it as `VEECODE_APP_CONFIG`. The entrypoint decodes it into
`/app/app-config.saas.yaml` (position 7), which wins over everything — preset
configs, plugin configs, and any mounted `local.yaml`. Use it for
deployment-specific values (database URLs, ingress hosts, secret references)
that must not be hardcoded.
:::
---
## Variable substitution
Any value in any config file may contain `${VAR}` or `${VAR:-default}`.
Backstage resolves these from the process environment at startup, **after** all
`--config` files are merged.
- `${VAR}` — replaced with the env value; if the variable is unset it resolves
to an empty string (Backstage does not error).
- `${VAR:-default}` — replaced with the env value if set; falls back to
`default` otherwise.
If a preset declares a variable as `required: true`, the entrypoint validates it
**before** Backstage starts. A missing required var exits with code **78**, so
substitution never runs on an incomplete environment. See
[Presets](./presets.md) for the full validation flow.
Substitution applies equally to all files in the chain. The chain position only
controls which file's *containing key* wins, not the substitution outcome.
:::warning `app.title` is baked at build time
`app.title` is compiled into the frontend bundle at image build time and
**cannot be overridden at runtime** through `app-config.local.yaml` or any
later layer. Runtime config overrides apply to backend-read keys and frontend
config delivered at boot, but the window title is fixed in the built bundle.
:::
---
## Inspecting and debugging the chain
To see the assembled `--config` flags and their order:
```bash
docker logs 2>&1 | grep -E "EXTRA_ARGS|preset"
```
If a setting is not taking effect, check, in order:
1. Which layer is setting the conflicting value.
2. Whether a higher-priority layer (a later file in the chain) is overriding it.
3. Whether the key path is correct (e.g. `app.branding.theme.light.palette.*`).
4. Whether a `${VAR}` substitution resolved to something other than you expect.
Backstage does not expose a stock "merged config" endpoint. Verify a setting
either by reading the boot logs (many keys emit a `Found N config(s)` or
`Configured for ...` line) or by testing the behavior the key controls.
---
## Related
- [Presets](./presets.md) — the preset model and how preset configs (layer 4)
are generated.
- [Dynamic Plugins](./dynamic-plugins.md) — how
`app-config.dynamic-plugins.yaml` (layer 6) is built.
- [Composing a Portal](./portal-composition.md) — how `app-config` backend
sections relate to plugin loading and entity annotations.
- For branding-specific keys, see [Simple Branding](../customization/branding.md).
---
## Dynamic Plugins
Dynamic plugins are the mechanism DevPortal uses to add Backstage plugins
**without rebuilding the container image**. A dynamic plugin is loaded at
runtime into `/app/dynamic-plugins-root/` rather than compiled into the
application bundle.
---
## Loading is step 1 of 3
This doc covers how plugins are packaged and loaded. Loading is necessary but not sufficient — a loaded plugin does nothing visible until two more steps are in place:
1. **Load** (a preset or `dynamic-plugins.yaml`) — this doc. Makes the plugin's code available.
2. **Context** (entity annotations in `catalog-info.yaml`) — tells the plugin which catalog entities it should attach to. Without the correct annotation on an entity, the plugin is loaded but idle.
3. **Backend** (`app-config`) — provides the credentials and endpoints the plugin queries. Without this, the plugin's tab appears but shows an error or empty state.
All three must be in place before a developer sees live data. See [Composing a Portal](./portal-composition.md) for the full model and a worked example.
---
## Static core vs. dynamic plugins
The image splits its plugins into two categories, mirroring Red Hat Developer
Hub's approach: a thin static core handles identity and data, and the feature
surface expands through the runtime plugin directory.
- **Static plugins** are registered directly in the backend
(`packages/backend/src/index.ts`): auth, catalog, scaffolder, search,
notifications, kubernetes backend, permissions, and the RBAC backend. Many
integration presets only *configure* these static plugins (via `appConfig:`)
and ship an empty `plugins: []` list.
- **Dynamic plugins** are everything else — every UI tab, every optional
catalog provider, every CI integration. They are pulled at boot and installed
into `/app/dynamic-plugins-root/`.
A handful of **core chrome** dynamic plugins ship pre-installed and always-on
(no `disabled:` field): `veecode-homepage`, `veecode-global-header`, the About
page and its backend, `dynamic-plugins-info`, and the marketplace catalog entity
provider. They are extracted into the image at build time and need no preset.
---
## The reference catalog: `dynamic-plugins.default.yaml`
`dynamic-plugins.default.yaml` is the **reference catalog** (the *vitrine*) — a
browsable list of every optional plugin the image bundles, with package names and
default OCI references. It is **not part of the boot chain**.
Every optional entry is listed with `disabled: true` as its default state. Use
the catalog to look up a plugin's package name or OCI reference before enabling
it via a preset or `dynamic-plugins.yaml`. A plugin not listed here works fine
if declared directly in `dynamic-plugins.yaml`.
Do not edit it to enable a plugin for one deployment — it is image-level
documentation. Use one of the selection surfaces below instead.
---
## How plugins get enabled — the three selection surfaces
A plugin is enabled if **any** selection surface includes it. There are three:
### 1. Presets (`VEECODE_PRESETS`) — recommended
Presets enable the plugins they declare. Each preset entry is self-contained — it
carries `package:`, `disabled: false`, and the full `pluginConfig:` (mount
points, dynamic routes, RBAC scopes, menu items) inline. No merge with
`dynamic-plugins.default.yaml` is needed at runtime. See [Presets](./presets.md).
### 2. Operator override — mounted `dynamic-plugins.yaml`
Mount a `dynamic-plugins.yaml` (read-only bind mount, or a Kubernetes ConfigMap)
with a top-level `plugins:` list. The entrypoint copies it to a writable shadow
and rebuilds the `includes:` chain on every boot, preserving your `plugins:`
entries. Because the operator's `plugins:` list is processed **last**, toggling
`disabled: true/false` here always wins over preset fragments. Apply changes
with `docker compose restart` (the entrypoint re-runs at boot).
### 3. Marketplace UI
The in-portal marketplace (`/marketplace`, enabled by the
`recommended` preset) lets end users install and uninstall plugins. The
marketplace backend writes selections to `/app/data/extensions-install.yaml`,
which is included in the plugin chain on the next restart and **survives
container restarts** as long as the `/app/data` volume is retained. (This is why
`/app/data` must be a directory volume, not a single-file bind mount.)
:::note The reference catalog and the marketplace index are independent
`dynamic-plugins.default.yaml` (reference catalog) and the marketplace's
`plugin-catalog-index` are two independent artifacts that share content but are
maintained separately. Editing one does not sync the other. The unification of
the two is a deferred decision — see
[ADR-013](https://github.com/veecode-platform/devportal-platform) in the
`devportal-platform` repo.
:::
For the full precedence table when surfaces conflict, see
[Adding Plugins](/devportal/plugins/adding).
---
## OCI reference shape
OCI plugin references used in presets and `dynamic-plugins.yaml` follow this pattern:
```
oci://${PLUGIN_REGISTRY}/:bs_${BACKSTAGE_VERSION}!
```
For example:
```yaml
# RBAC UI — workspace "rbac", selector "backstage-community-plugin-rbac"
- package: oci://${PLUGIN_REGISTRY}/rbac:bs_1.49.4!backstage-community-plugin-rbac
# Marketplace frontend — version tracked via BACKSTAGE_VERSION
- package: oci://${PLUGIN_REGISTRY}/marketplace:bs_${BACKSTAGE_VERSION}!devportal-marketplace-frontend-dynamic
```
The four parts:
- **`${PLUGIN_REGISTRY}`** — defaults to `quay.io/veecode`; substituted by
`entrypoint.sh`. Override it (e.g. `PLUGIN_REGISTRY=registry.internal/veecode`)
to redirect all OCI pulls that use the `${PLUGIN_REGISTRY}` variable form (the
large majority of bundled plugins) to an internal mirror without editing any YAML.
- **``** — the export-overlays workspace that produced the bundle
(e.g. `marketplace`, `rbac`, `tech-radar`, `sonarqube`, `backstage`). One
workspace can bundle several packages.
- **`bs_${BACKSTAGE_VERSION}`** — the OCI tag. `${BACKSTAGE_VERSION}` is
substituted from `backstage.json`, so a Backstage bump propagates to every
reference that uses the variable form. Some entries pin a literal version
(e.g. `bs_1.48.4`, `bs_1.49.4`) when the plugin has not yet been re-published
under the current tag.
- **`!`** — the specific npm package name inside the bundle.
Pre-installed chrome plugins use a bare npm package name (no `oci://` prefix)
with `preInstalled: true`; the install script skips the pull and only merges
their `pluginConfig:`.
:::caution MCP plugin refs are hardcoded and not redirected by `PLUGIN_REGISTRY`
The MCP plugin refs (`mcp-actions-backend`, `mcp-integrations`, `mcp-chat`) are
hardcoded to `quay.io/veecode` and do **not** use the `${PLUGIN_REGISTRY}`
variable form. Setting `PLUGIN_REGISTRY` does not redirect them. Air-gapped or
mirror deployments using the `mcp` or `mcp-chat` presets must mirror those
workspaces (`quay.io/veecode/backstage`, `quay.io/veecode/mcp-integrations`,
`quay.io/veecode/mcp-chat`) explicitly, or override the refs in a
`dynamic-plugins.yaml`.
:::
---
## Boot sequence (what installs your plugins)
At container start, before Backstage accepts requests:
1. **Preset resolver** validates required env vars (exit 78 on missing) and
writes a `preset--plugins.yaml` for each preset with a `plugins:` list.
2. **Assemble the includes chain** — `dynamic-plugins.yaml` is copied to a
writable working file and the `includes:` chain is rebuilt to reference the
marketplace file and each preset's plugin file.
3. **`${BACKSTAGE_VERSION}` and `${PLUGIN_REGISTRY}` substitution** across the
working files.
4. **`install-dynamic-plugins.py`** — for each enabled entry, `skopeo copy`
pulls the OCI bundle, the named selector is extracted into
`/app/dynamic-plugins-root//`, and the entry's `pluginConfig:` is
merged into `/app/dynamic-plugins-root/app-config.dynamic-plugins.yaml`.
Pre-installed entries skip the pull.
5. **Backend boot** — Backstage reads `app-config.dynamic-plugins.yaml` to
discover mount points, dynamic routes, and RBAC scopes.
Loaded plugins are surfaced at
`/api/dynamic-plugins-info/loaded-plugins` once the backend is up.
:::warning Boot fails fast (exit 78) on plugin errors
If a plugin bundle cannot be pulled (registry unreachable, typo'd OCI ref,
missing mirror bundle), the install script prints an `INSTALL SUMMARY` of failed
refs and the entrypoint exits **78** rather than booting a half-installed
portal. The same exit-78 guard fires when the same plugin is enabled with two
different OCI refs (the duplicate detector). For dev iteration only, set
`DYNAMIC_PLUGINS_TOLERATE_FAILURES=true` to proceed with whatever installed.
:::
---
## Distribution modes
Three modes are supported by design:
- **Default — runtime OCI pull.** The image ships with no optional plugin bytes.
At boot, `install-dynamic-plugins.py` pulls each enabled plugin from
`quay.io/veecode/:`. No operator config beyond
`VEECODE_PRESETS`. Best for cloud/SaaS with outbound registry access.
- **Mirror — internal registry.** Set `PLUGIN_REGISTRY=registry.internal/veecode`
(or any prefix mirroring `quay.io/veecode`). The entrypoint substitutes it
into every `oci://${PLUGIN_REGISTRY}/...` reference before the install runs —
no YAML edits needed for the plugins that use the variable form. The mirror must
host the same workspace/tag paths. Note that the MCP plugin refs
(`mcp-actions-backend`, `mcp-integrations`, `mcp-chat`) are hardcoded to
`quay.io/veecode` and are not redirected; see the caution in the
[OCI reference shape](#oci-reference-shape) section above.
- **Loaded variant — air-gapped image.** Build a derived image that extracts the
selected plugin bundles at build time and copies them into
`/app/dynamic-plugins-root/`. Mark those entries `preInstalled: true` so the
install script skips the pull. Pre-baked variants are the operator's
responsibility; the published image stays generic.
---
## Related
- [Presets](./presets.md) — the recommended way to enable plugins.
- [Configuration Hierarchy](./configuration-hierarchy.md) — how
`app-config.dynamic-plugins.yaml` fits the config merge order.
- [Adding Plugins](/devportal/plugins/adding) — the selection surfaces in
practice and the full precedence rules.
---
## Environment Cluster Journey by Veecode Platform
Welcome to the Environment Cluster journey. This guide explains the purpose of this approach and how to use it.
# Environment Cluster Journey by Veecode Platform
Our environment cluster journey is designed to make the development process more dynamic and independent. Once the DevOps team has set up the environments and the types of clusters available, developers can deploy their applications swiftly and accurately.
## How It Works
Both **Environments** and **Clusters** are modeled as `Resource` entities in the Software Catalog. The journey works as follows:
1. **Environment Creation:** The DevOps team manually inserts an Environment entity into the catalog. An environment stores configuration and credential references: network settings, environmental variables, service access credentials, and more. Environments are currently registered manually via a `catalog-info.yaml` with `kind: Resource` and `spec.type: environment`.
2. **Cluster Configuration:** A Cluster is a `Resource` entity that references an environment. Clusters are typically created via a scaffolder IaC template — the template uses the environment's configuration as input (network, region, credentials) and applies cluster-specific parameters (name, machine size, repository). Once the template runs, the cluster entity is registered in the catalog.
3. **Application Deployment:** With environments and clusters in the catalog, developers can reference them in software templates and deploy their applications to the correct target infrastructure.
This approach creates a clear separation of concerns: the DevOps team controls environment and cluster definitions; developers consume them self-service.
Because environments and clusters are catalog entities, they inherit everything the catalog provides — ownership, RBAC, search, and annotations. The same composition model that wires a `Component` to its CI plugin can wire a `Resource` (cluster) to a Grafana dashboard for cluster-level observability. See [Composing a Portal](./portal-composition.md).
**Summary:** Environments are utilized by clusters, and clusters provide the infrastructure for deploying final projects.
---
## Using Cluster Templates
The steps below describe the general flow for creating a cluster via a scaffolder template. The exact fields depend on the specific template your organization has published.
1. **Prepare your environment:** Before you start, make sure you have an environment `Resource` entity registered in the catalog.
2. **Access DevPortal:** Log in and select **"Create"** from the sidebar menu.
3. **Choose a cluster template:** Select the cluster provisioning template relevant to your target infrastructure.
4. **Select Environment:** Choose the desired environment from the available resources, then proceed.
5. **Configure cluster parameters:** Fill in resource-specific fields (machine size, region, network settings, etc.) as required by the template.
6. **Set up the repository:** Select your Git provider, specify the owner, and name the repository the template will create. Set repository visibility.
7. **Review and Create:** Review all the information, then click **"Create"**.
8. **Monitor the pipeline:** The scaffolder creates the repository and triggers a CI/CD pipeline. Monitor progress in the scaffolder log view. Ensure all required secrets (cloud credentials, tokens, etc.) are configured in your CI/CD environment before the pipeline runs.
9. **Locate the cluster in the catalog:** Once the pipeline completes and the entity is registered, navigate to the **Catalog**, filter by `Kind: Resource`, and look for your cluster by name.
:::info
There is no dedicated "Resources > Clusters" sidebar entry. Clusters and environments appear in the standard Catalog view — filter by `Kind: Resource` or use the search to find them.
:::
:::warning
Make sure all required CI/CD secrets (e.g., cloud provider credentials) are set before initiating the pipeline. Missing secrets will cause the pipeline to fail.
:::
If you encounter any issues, refer to the platform's documentation or reach out to the support team.
---
## Infrastructure as Code (IaC) Templates
This guide explains how **Infrastructure as Code (IaC) templates** work in DevPortal, enabling your team to provision infrastructure through a self-service scaffolder workflow rather than running tools manually.
---
### Step 1: **Understand the Basics**
IaC templates in DevPortal are scaffolder templates (like software templates) that generate infrastructure repositories — typically containing Terraform, CloudFormation, Pulumi, or similar IaC files. Provisioning happens through the CI/CD pipeline that the template wires up, not by the developer running infrastructure tools locally.
The typical flow is:
1. Developer fills in the template form (resource sizes, region, network settings, etc.).
2. The scaffolder creates a new repository with the IaC configuration files.
3. A CI/CD pipeline (GitHub Actions, GitLab CI, etc.) is triggered and runs the infrastructure tool (e.g., `terraform apply`).
4. The resulting infrastructure resource is registered in the catalog as a `Resource` entity.
---
### Step 2: **Access Available Templates**
1. Log in to your **Developer Portal**.
2. Navigate to the **Templates Page** and explore the list of preconfigured IaC templates available.
---
### Step 3: **Select the Appropriate Template**
- Review the available templates and select the one that aligns with your project needs.
- Consider resource types (e.g., virtual machines, storage), deployment scale, and additional infrastructure requirements.
---
### Step 4: **Customize the Template**
1. Read the documentation associated with the chosen template.
2. Fill in the form parameters, which typically include:
- **Resource sizes:** CPU, memory, or storage requirements.
- **Network configurations:** subnets, routing, and security rules.
- **Security settings:** permissions or encryption protocols.
- **Repository settings:** provider, owner, repository name, visibility.
---
### Step 5: **Create and Monitor the Pipeline**
1. Review all inputs on the **Overview Page** and click **"Create"**.
2. The scaffolder creates the IaC repository and triggers the CI/CD pipeline.
3. Monitor the creation log in the DevPortal scaffolder UI for real-time updates.
4. Once the pipeline completes, the infrastructure resource may be registered in the catalog automatically (depending on the template) or require manual registration.
:::info
You do not need to run `terraform apply`, `aws cloudformation deploy`, or any other infrastructure CLI locally. The CI/CD pipeline configured by the template handles deployment in your target environment.
:::
---
### Step 6: **Validate the Infrastructure**
- Verify that the deployed infrastructure matches your requirements.
- Test the environment for functionality, security, and stability using your standard validation tooling.
---
### Tips for Using IaC Templates
- **Leverage Version Control:** All IaC files are stored in Git — use PRs and code review for infrastructure changes.
- **Promote Collaboration:** Developers and operations teams co-own the generated repository.
- **Document Changes:** The catalog entity for the resource tracks ownership and metadata; keep it updated.
---
### Benefits of IaC Templates
1. **Consistency:** Eliminate configuration drift by ensuring uniform deployments.
2. **Scalability:** Easily replicate infrastructure to meet growing demands.
3. **Self-Service:** Developers can provision infrastructure without waiting for manual ops work.
By adopting IaC templates, you enable your team to create and manage infrastructure efficiently through the portal, with CI/CD as the enforcement layer.
---
## IaC templates in the composition model
An IaC template execution produces the same catalog artifacts as a software template: a repository, a `catalog-info.yaml`, and a registered `Location` entity — except the entity kind is `Resource` rather than `Component`. That resource entity appears in the catalog, carries ownership metadata, and can have annotations that attach it to observability plugins (Grafana dashboards scoped to the provisioned infrastructure, for example).
The resource entity is the catalog's representation of the infrastructure. It is queryable via the Relations API, visible on dependency graphs, and subject to the same RBAC rules as any other entity.
### Template-of-templates: app + infra as a single Golden Path
An IaC template can be a step inside a software template. The software template orchestrates both: it scaffolds the application repository and invokes the IaC template (or an equivalent scaffolder action) to provision the required infrastructure in the same workflow. The developer fills in one form; both a `Component` entity and a `Resource` entity emerge, linked via the `dependsOn` relation in `catalog-info.yaml`.
This is the mechanism behind a complete [Golden Path](/platform/concepts/golden-paths): the template encodes the organization's standard for what a production-ready service looks like — application code, CI/CD configuration, and infrastructure — as a single self-service operation.
#### Declaring the `dependsOn` link
The link between Component and Resource is a standard Backstage relation, written into the Component's `catalog-info.yaml` under `spec.dependsOn[]`. The template emits it as part of the scaffolded file — the developer never writes it by hand.
```yaml
# catalog-info.yaml emitted by the software template for the Component
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-service
annotations:
backstage.io/kubernetes-label-selector: 'app=payment-service'
gitlab.com/project-slug: my-group/payment-service
spec:
type: service
lifecycle: production
owner: checkout-team
system: checkout-system
dependsOn:
- resource:default/payment-db # provisioned by the IaC template step
- resource:default/payment-queue
```
The referenced Resources are produced by the IaC template invocation inside the same workflow. Once both entities are registered, the Component's overview page shows the dependency graph; the Resources show the reverse `dependencyOf` relation. Catalog providers, RBAC rules, and observability plugins all see the link.
The format is `:/` (lowercase), matching Backstage's entity reference convention.
---
For the three-layer model that governs how the resulting entities connect to plugins, see [Composing a Portal](./portal-composition.md). For the software template side of this pattern, see [Software Templates](./software-template.md).
---
## See also
- [Writing Templates](./writing-templates) — YAML authoring guide for template authors
- [Available Actions](./available-actions) — full list of pre-registered actions
---
For further assistance, refer to your platform's documentation or [contact us](https://platform.vee.codes/support/).
---
## Composing a Portal
A DevPortal installation out of the box is a service catalog and a template runner. Teams can register their services, create new ones from templates, and browse the software landscape. That is Day-0: the portal knows what exists and can create things, but it doesn't connect to anything live yet.
The value engineers actually care about — seeing pod status, triggering a deployment, checking code quality, browsing dashboards without leaving the service page — comes from plugin composition. Understanding how composition works is what separates "I set `VEECODE_PRESETS`" from "I built an operational hub."
---
## Three levels of composition
Every plugin activates across three layers. All three must be in place before a developer sees anything.
### 1. Load — presets or `dynamic-plugins.yaml`
This controls which plugins are present at all. In V2 there are two ways to load a plugin, and a plugin is loaded if **either** path selects it:
- **A preset** (recommended). Setting `VEECODE_PRESETS=...,kubernetes` flips the Kubernetes plugin from `disabled: true` to `disabled: false` and supplies its baseline `appConfig`. This is the curated fast path. See [Presets](./presets.md).
- **An operator override.** Mount a `dynamic-plugins.yaml` with a top-level `plugins:` list to enable a bundled plugin or pull an external OCI/npm one that no preset covers.
```yaml
# dynamic-plugins.yaml — enable a bundled plugin no preset covers
plugins:
- package: oci://${PLUGIN_REGISTRY}/grafana:bs_${BACKSTAGE_VERSION}!backstage-plugin-grafana
disabled: false
```
Enabling a plugin loads its code: UI components and backend routes become available. But nothing appears to the developer yet.
### 2. Context — entity annotations
Plugins are **context-aware by design**. They do not add global tabs. They add tabs and cards to specific catalog entities — and only when those entities carry the right annotation.
```yaml
# catalog-info.yaml
metadata:
annotations:
backstage.io/kubernetes-label-selector: 'app=my-service'
```
Without this annotation on the entity, the Kubernetes plugin is loaded but idle. With it, the Kubernetes tab appears on that entity only, and shows pods matching the selector.
This is intentional. A platform with dozens of services doesn't need every plugin visible on every entity. Each service declares what it uses. The portal surfaces exactly what belongs to that service.
### 3. Backend — `app-config`
The tab appears. But it needs to know where to fetch data from. The backend configuration provides that. A preset supplies a baseline (for `kubernetes`, the cluster wired from `K8S_CLUSTER_URL` / `K8S_CLUSTER_TOKEN`); anything beyond the baseline goes in your `app-config.local.yaml`:
```yaml
kubernetes:
clusterLocatorMethods:
- type: config
clusters:
- name: production
url: ${K8S_CLUSTER_URL}
serviceAccountToken: ${K8S_CLUSTER_TOKEN}
```
Without this, the tab loads and shows an error or empty state. With all three layers in place, the tab displays live data.
### The diagnostic model
A plugin that fails to show data has failed at one of these three levels. Diagnose in layer order — load before context before backend, because each layer assumes the previous one succeeded.
| Symptom | Layer | Fix |
|---|---|---|
| Container won't boot (crash loop / exited), or plugin absent on every entity | Load — install failed or plugin not enabled | See [Diagnosing the Load layer](#diagnosing-the-load-layer) below |
| Tab not visible on a specific entity | Context — annotation missing | Add the annotation to that entity's `catalog-info.yaml` |
| Tab visible, empty or error | Backend — configuration missing or unreachable | Check `app-config` for the relevant integration |
#### Diagnosing the Load layer
This is where V2 differs sharply from the V1 distro. **V2 fails fast.** When `install-dynamic-plugins.py` cannot install an enabled plugin — typo'd OCI ref, registry unreachable, wrong `PLUGIN_REGISTRY` mirror, integrity mismatch — it prints an `INSTALL SUMMARY` and the entrypoint **aborts the boot with exit code 78** rather than starting a half-installed portal. So the usual symptom is not "the portal is up but the plugin is missing" — it is "**the container never came up**" (a crash loop in Kubernetes, an exited container under Docker).
Check the container logs for the install lifecycle lines:
```bash
docker logs devportal 2>&1 | grep -E "======= (Installing|Skipping|Using pre-installed|ERROR|INSTALL SUMMARY)"
```
A healthy load:
```
======= Installing dynamic plugin oci://quay.io/veecode/backstage:bs_1.49.4!...
==> Successfully installed dynamic plugin oci://quay.io/veecode/backstage:bs_1.49.4!...
```
A failed load ends with the summary that triggers exit 78:
```
======= ERROR: Failed to install plugin oci://quay.io/veecode/backstage:...:
==> Skipping this plugin and continuing with the rest...
======= INSTALL SUMMARY: 1 of 12 plugins failed:
- oci://quay.io/veecode/backstage:...:
```
Common failure signatures and what they mean:
| Log signature | What happened | Likely cause |
|---|---|---|
| `======= ERROR: Failed to install plugin ... npm ERR! 404` | npm package or version doesn't exist | Typo in `package:`, or version not published |
| `======= ERROR: Failed to install plugin ... skopeo ... non-zero exit status 1` | OCI image not found or unreachable | Wrong workspace/tag, registry unreachable, or a bad `PLUGIN_REGISTRY` mirror prefix — see [Finding the OCI reference](/devportal/plugins/adding) |
| `======= ERROR: ... hash of the downloaded package ... does not match the provided integrity hash` | Tampered or wrong-version artifact | Regenerate or remove the `integrity:` field, or set `SKIP_INTEGRITY_CHECK=true` for a trusted source |
| `VEECODE: FATAL — /app/dynamic-plugins.yaml is not valid YAML; aborting boot` | Your mounted `dynamic-plugins.yaml` doesn't parse | YAML syntax error — boot aborts **before** install runs |
| `InstallException: Config key '...' defined differently for 2 dynamic plugins` / duplicate ref rejection | The same plugin is enabled via two different refs | Reconcile the preset and your operator override to a single ref |
| No `Installing`/`Skipping` line for the plugin at all | The package was never enabled | It is `disabled: true`, not selected by any preset, and not in your `dynamic-plugins.yaml` |
:::caution The fail-fast default is deliberate — and overridable
Booting with a half-installed plugin set is a footgun (the UI silently misses tabs), so V2 aborts instead. For dev iteration or a known-flaky upstream plugin, set `DYNAMIC_PLUGINS_TOLERATE_FAILURES=true`. The installer then completes a **partial** install and the portal boots **without** the failed plugins — at which point the failure mode reverts to the V1-style "plugin silently absent," and you must read the `INSTALL SUMMARY` lines above to know what's missing.
:::
#### Diagnosing "tab appears but is empty or errors"
The plugin is loaded and the annotation is correct, but the data isn't showing. Check in this order:
1. **Backend section exists in the merged config** — for Kubernetes: `kubernetes.clusterLocatorMethods`. For SonarQube: `sonarqube.baseUrl` + `apiKey`. A preset supplies a baseline; anything else goes in `app-config.local.yaml`. Without the backend config, the plugin frontend has no source to query.
2. **The annotation value matches reality** — `backstage.io/kubernetes-label-selector: 'app=my-service'` must match the actual labels on the pods in the cluster. If the pods are labeled `app=myservice` (no hyphen), the plugin returns empty with no UI error.
3. **Credentials and reachability** — the service account token, OAuth credentials, or API key must have read access, and the DevPortal container's network must reach the external system.
4. **TLS settings** — for self-signed certs in `kubernetes.clusterLocatorMethods`, set `skipTLSVerify: true`. Otherwise the connection fails silently from the frontend's perspective.
5. **Backend logs** — connection errors surface in the container logs, not the UI:
```bash
docker logs devportal | grep -i
```
---
## The Day-0 → Day-1 → Day-2 progression
This isn't a checklist you complete once. It is a deliberate sequence.
### Day-0: Foundation
- Catalog populated — services, APIs, and resources registered via `catalog-info.yaml`
- Software templates defined — teams can create new services following the organization's Golden Paths
- Authentication and SCM configured via `VEECODE_PRESETS` — an identity preset (`github-auth`, `gitlab`, `keycloak`, …) sets the sign-in provider, and an SCM preset (`github`, `gitlab`, `azure`) wires catalog discovery and repository integration (see [Presets](./presets.md))
The portal knows what exists. Engineers can create new services from opinionated templates. Value: reduced onboarding time and consistent project structure.
### Day-1: Connecting services to their tooling
- Enable plugins — add the relevant preset (`kubernetes`, `sonarqube`, `jenkins`) or, for plugins no preset covers, an entry in `dynamic-plugins.yaml`
- Add annotations to catalog entities — each annotation is a claim: "this entity owns this Kubernetes workload / this GitLab project / this SonarQube project"
- Configure backends — preset baselines plus any overrides in `app-config.local.yaml`
### Day-2: The operational hub
- Developers operate services without leaving the portal
- CI/CD tabs show pipeline status alongside the service entity
- Kubernetes tab shows pod health and live logs
- Code Quality tab surfaces SonarQube results
The portal is no longer a catalog. It is the operational interface for the service.
---
## Example: internal team portal
A concrete composition — GitLab CI, Kubernetes workloads, and SonarQube code quality — across the three layers. GitLab, Kubernetes, and SonarQube are all presets, so the Load and baseline-Backend layers come from `VEECODE_PRESETS`; the Context layer is per-entity annotations.
**Load + auth + SCM + baseline backend — `VEECODE_PRESETS` and its variables:**
```sh
docker run -p 7007:7007 \
-e VEECODE_PRESETS=recommended,gitlab,kubernetes,sonarqube \
-e GITLAB_HOST=gitlab.com \
-e GITLAB_AUTH_CLIENT_ID=xxx -e GITLAB_AUTH_CLIENT_SECRET=xxx \
-e GITLAB_TOKEN=xxx -e GITLAB_GROUP=my-group \
-e K8S_CLUSTER_NAME=production -e K8S_CLUSTER_URL=https://k8s.internal:6443 \
-e K8S_CLUSTER_TOKEN=xxx \
-e SONARQUBE_BASE_URL=https://sonar.internal -e SONARQUBE_API_KEY=xxx \
-v $(pwd)/app-config.local.yaml:/app/app-config.local.yaml:ro \
veecode/devportal:2.1.3
```
**Each service's `catalog-info.yaml` — Context, what it owns:**
```yaml
metadata:
annotations:
backstage.io/kubernetes-label-selector: 'app=my-service,env=production'
gitlab.com/project-slug: my-group/my-service
sonarqube.org/project-key: my-group_my-service
```
**`app-config.local.yaml` — Backend overrides beyond the preset baseline:**
```yaml
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- name: production
url: ${K8S_CLUSTER_URL}
serviceAccountToken: ${K8S_CLUSTER_TOKEN}
skipTLSVerify: true # self-signed cluster cert
```
Result: each service entity has a Kubernetes tab, a GitLab CI tab, and a Code Quality tab — each scoped to that service by its annotations. Developers see the operational state of their service in one place.
:::note Plugins without a preset
Not every plugin has a dedicated preset (Kubernetes, GitHub Actions, Jenkins, and others ship in the catalog disabled and are enabled via `dynamic-plugins.yaml` or the marketplace UI). The composition model is identical regardless of how a plugin is loaded — once loaded, it still follows the Context (annotation) and Backend (`app-config`) rules above. See [Adding Plugins](/devportal/plugins/adding) and the [Bundled Plugin Catalog](/devportal/plugins/bundled).
:::
---
## Choosing the right plugins for your context
The question isn't "which plugins are available" — it's "what does my team need to stop context-switching for?"
| If your team uses... | Enable... | Required annotation |
|---|---|---|
| GitHub CI | GitHub Actions | `github.com/project-slug: org/repo` |
| GitLab CI | GitLab Pipelines (OCI-only — add via `dynamic-plugins.yaml`; the `gitlab` preset covers auth + catalog only) | `gitlab.com/project-slug: group/project` |
| Jenkins | Jenkins (`jenkins` preset) | `jenkins.io/job-full-name: folder/job` |
| Azure Pipelines | Azure DevOps (`azure` preset) | `dev.azure.com/project-repo: project/repo` |
| Kubernetes | Kubernetes (`kubernetes` preset) | `backstage.io/kubernetes-label-selector: app=name` |
| Grafana | Grafana | `grafana/dashboard-selector: "tags @> 'name'"` |
| SonarQube | SonarQube (`sonarqube` preset) | `sonarqube.org/project-key: project-key` |
| HashiCorp Vault | Vault | `vault.io/secrets-path: secret/data/service` |
For plugins not in the bundled set, see [Finding Plugins](/devportal/plugins/finding).
---
## References
- [Presets](./presets.md) — how `VEECODE_PRESETS` sets Day-0 auth, SCM, and the curated plugin set
- [Dynamic Plugins](./dynamic-plugins.md) — the load layer in depth: catalog, selection surfaces, OCI references
- [The Catalog](./catalog.md) — entity kinds, ownership, and how `catalog-info.yaml` is processed
- [Adding Plugins](/devportal/plugins/adding) — OCI and npm download configuration for plugins no preset covers
- [Bundled Plugin Catalog](/devportal/plugins/bundled) — what ships in the image and its default state
---
## Presets
# Presets (`VEECODE_PRESETS`)
DevPortal ships as a single unified image. A **preset** is what turns that
generic image into a working portal for a specific stack. Presets are
selected at runtime by setting the `VEECODE_PRESETS` environment variable to a
comma-separated list of names:
```sh
VEECODE_PRESETS=recommended,veecode-theme,github
```
The entrypoint resolves each listed preset **before** Backstage starts. If any
required environment variable is missing, the boot fails fast with exit code
**78** and names every missing variable at once.
:::note Presets replace the V1 `VEECODE_PROFILE`
In the previous distro model, a single `VEECODE_PROFILE` selected one
`app-config..yaml`. V2 replaces that one-of-seven choice with
**composable** presets: you stack as many as you need, and they layer in order.
:::
---
## What a preset is
A preset is a versioned YAML file at `presets/.yaml` that declares three
things:
| Field | Purpose |
| --- | --- |
| `requires.variables` | The environment variables the operator must supply, each with a description, `required` flag, optional `docs` URL, and `example`. |
| `plugins` | The dynamic plugins the situation needs. Each entry is self-contained: `package:` (OCI reference), `disabled: false`, and the full `pluginConfig:` block inline. |
| `appConfig` | The Backstage `app-config` block those plugins expect. Written to its own `app-config.preset-.yaml` and passed to Backstage as a `--config` file. |
Many integration presets carry an empty `plugins: []` list — their backend
plugins (catalog, auth, scaffolder modules) are **static** (compiled into the
backend) and the preset only *configures* them via `appConfig`. See
[Dynamic Plugins](./dynamic-plugins.md) for the static-vs-dynamic split.
---
## How composition works at boot
`VEECODE_PRESETS=a,b,c` triggers the preset resolver in `entrypoint.sh`. For
each preset, in list order, it runs three steps:
1. **Variable validation** — for every `requires.variables` entry marked
`required: true`, the resolver checks the environment. It accumulates **all**
missing variables across **all** selected presets, then prints the combined
error and exits 78. A single boot attempt surfaces every missing variable for
the full list, not just the first one.
2. **Plugin fragment** — if the preset's `plugins:` list is non-empty, the
resolver writes `/app/preset--plugins.yaml` and adds it to the plugin
includes chain processed by `install-dynamic-plugins.py`.
3. **App-config fragment** — if the preset's `appConfig:` block is non-empty,
the resolver writes `/app/app-config.preset-.yaml` and appends it to
Backstage's `--config` argument list.
Backstage's native config loader **deep-merges** the `--config` files: object
keys merge recursively, scalar keys are last-write-wins in **preset order**. No
manual merge logic runs. Plugin entries merge shallow per `package:` key, so if
two presets enable the same plugin, the later preset wins.
An operator-mounted `app-config.local.yaml` always wins over preset-generated
configs. See [Configuration Hierarchy](./configuration-hierarchy.md) for the
full precedence chain.
:::warning The `package:` value must be unique across all sources
If the same plugin is declared in both a preset and `dynamic-plugins.yaml`, the
`package:` strings must be identical — including the `${PLUGIN_REGISTRY}` and
`${BACKSTAGE_VERSION}` variable forms. A mismatch causes the install script to
treat them as two distinct plugins, installing the bundle twice and crashing the
backend on duplicate registration.
:::
---
## Inspecting what a preset configured
A preset is not a switch or a hidden mode — it is plain config the resolver
writes to disk **before** Backstage starts. If GitHub sign-in works, the catalog
populated, and templates target your org, that behavior came from files you can
read inside the running container:
```bash
# the app-config a preset contributed (layer 4)
docker exec devportal cat /app/app-config.preset-github.yaml
# the plugin fragment a preset enabled (present only when the preset enables plugins)
docker exec devportal cat /app/preset-github-plugins.yaml
```
Replace `github` with any selected preset. These files are the complete source of
truth for what the preset configured. If something works that you didn't
explicitly set, it came from a preset — and you override any of it by adding the
key to `app-config.local.yaml`, which loads after every preset layer and wins
(see [Configuration Hierarchy](./configuration-hierarchy.md)). The boot log also
echoes the assembled includes chain (`VEECODE: dynamic plugin includes → …`) and
the final `--config` list (`EXTRA_ARGS=…`).
---
## Tiers
Every plugin in the image falls into one of three tiers:
- **Core** — always on, baked into the image, gated by no preset. The global
header (search, notifications, profile), the homepage, the About page and its
backend, and `dynamic-plugins-info`. The portal is unusable without these and
they need zero configuration.
- **`recommended`** — enabled by `VEECODE_PRESETS=recommended`. Adds the
DevPortal marketplace (front + back), the pending-changes widget, a tech-radar
with sample data, and the RBAC UI. Works with zero configuration and makes the
image read as a DevPortal rather than a bare Backstage shell.
- **Integration presets** — enabled only when selected; each integrates with
something customer-specific and therefore declares `requires.variables`.
---
## The SCM-vs-identity split
V2 separates **source-control integration** (SCM) from **sign-in identity**.
These are orthogonal axes you compose independently:
- `github` wires GitHub **as SCM** — catalog/repo discovery, integration, and
the GitHub Actions UI tab. It does **not** wire OAuth sign-in.
- `github-auth` wires GitHub **as identity** — OAuth sign-in plus org/team user
sync. Compose `github,github-auth` for the full GitHub stack, or compose
`github-auth` with a different SCM preset to use GitHub purely as the login
provider.
- `azure` (Azure DevOps as SCM) and `azure-auth` (Microsoft / Entra ID as
identity) split the same way.
- **GitLab is a single preset.** There is **no separate `gitlab-auth`** — the
`gitlab` preset wires both OAuth sign-in and repo/org catalog discovery.
### The `identity` exclusive group
Presets that provide sign-in declare `exclusive_group: identity`: `github-auth`,
`gitlab`, `azure-auth`, `keycloak`, and `ldap`. Only one identity provider can
be active at a time. (`ldap-ad` is an override-only layer on top of `ldap` and
is not itself an identity preset.)
### The `mcp,mcp-chat` dependent pair
`mcp` exposes the MCP server to external CLI clients (Claude Code, Codex CLI,
Cursor) and requires no LLM key. `mcp-chat` adds the in-portal AI chat at
`/mcp-chat` and talks loopback to the `mcp` backend, so it only works when
composed as `VEECODE_PRESETS=mcp,mcp-chat`. The schema cannot enforce this
dependency, so it is documented rather than validated.
---
## Shipped presets
Every preset in `presets/` at the current image tag. Each row is the operator's
contract — what enabling the preset gives you and what variables you must
supply.
| Preset | What it enables | Required env vars |
| --- | --- | --- |
| `recommended` | Marketplace (front + back), pending-changes, tech-radar (sample data), RBAC UI | none |
| `veecode-theme` | VeeCode brand palette + typography + logos/favicons | none |
| `github` | GitHub PAT integration + repo discovery + Actions UI tab. Does **not** wire OAuth sign-in | `GITHUB_PAT`, `GITHUB_ORG` |
| `github-auth` | GitHub OAuth sign-in + org/team user sync (identity group) | `GITHUB_PAT`, `GITHUB_ORG`, `GITHUB_AUTH_CLIENT_ID`, `GITHUB_AUTH_CLIENT_SECRET` |
| `gitlab` | GitLab OAuth sign-in + integration + repo/org catalog discovery (identity group) | `GITLAB_HOST`, `GITLAB_AUTH_CLIENT_ID`, `GITLAB_AUTH_CLIENT_SECRET`, `GITLAB_TOKEN`, `GITLAB_GROUP` |
| `azure` | Azure DevOps integration + catalog + pipelines/PR UI. Does **not** wire Microsoft sign-in | `AZURE_DEVOPS_TOKEN`, `AZURE_DEVOPS_HOST`, `AZURE_DEVOPS_ORG`, `AZURE_DEVOPS_PROJECT` |
| `azure-auth` | Microsoft (Entra ID) OAuth sign-in + msgraphOrg user sync (identity group) | `AZURE_AUTH_TENANT_ID`, `AZURE_AUTH_CLIENT_ID`, `AZURE_AUTH_CLIENT_SECRET` |
| `keycloak` | Keycloak / OIDC sign-in + keycloakOrg user/group sync (identity group) | `KEYCLOAK_BASE_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET`, `AUTH_SESSION_SECRET` |
| `ldap` | LDAP sign-in + ldapOrg user/group sync, OpenLDAP defaults (identity group) | `LDAP_URL`, `LDAP_DN`, `LDAP_SECRET`, `LDAP_USERS_BASE_DN`, `LDAP_GROUPS_BASE_DN` |
| `ldap-ad` | Active Directory overrides for `ldap` (sAMAccountName, AD object classes). **Compose with `ldap`** (compose with `ldap`; not a standalone identity preset) | none (reuses `ldap` vars) |
| `jenkins` | Jenkins CI tab on entity pages | `JENKINS_URL`, `JENKINS_USERNAME`, `JENKINS_TOKEN` |
| `kubernetes` | Kubernetes workloads tab on entity pages | `K8S_CLUSTER_NAME`, `K8S_CLUSTER_URL`, `K8S_CLUSTER_TOKEN` |
| `sonarqube` | SonarQube code-quality tab + scaffolder action | `SONARQUBE_BASE_URL`, `SONARQUBE_API_KEY` |
| `mcp` | MCP server at `/api/mcp-actions/v1` for external AI clients via OAuth/DCR | none |
| `mcp-chat` | AI chat UI at `/mcp-chat`. **Compose with `mcp`** (loopback dependency) | `MCP_CHAT_PROVIDER`, `MCP_CHAT_API_KEY`, `MCP_CHAT_MODEL` |
Required variables are **unioned** across the selected presets; the boot exits
78 listing every missing one.
---
## Examples
### Out-of-box VeeCode look (no required vars)
```sh
docker run -p 7007:7007 \
-e VEECODE_PRESETS=recommended,veecode-theme \
veecode/devportal:2.1.3
```
The evaluation starting point: VeeCode brand palette, marketplace, tech-radar,
RBAC UI, pending-changes widget.
### GitHub-integrated stack
```sh
docker run -p 7007:7007 \
-e VEECODE_PRESETS=recommended,veecode-theme,github \
-e GITHUB_PAT=ghp_xxxx \
-e GITHUB_ORG=my-org \
veecode/devportal:2.1.3
```
The `github` preset wires a catalog provider that scans `catalog-info.yaml`
files under `GITHUB_ORG`, the GitHub SCM integration, and the GitHub Actions UI
tab. To add OAuth sign-in, compose `github-auth` as well.
### Keycloak-authenticated stack
```sh
docker run -p 7007:7007 \
-e VEECODE_PRESETS=recommended,keycloak \
-e KEYCLOAK_BASE_URL=https://keycloak.internal \
-e KEYCLOAK_REALM=devportal \
-e KEYCLOAK_CLIENT_ID=devportal \
-e KEYCLOAK_CLIENT_SECRET=xxx \
-e AUTH_SESSION_SECRET=xxx \
veecode/devportal:2.1.3
```
---
## The curation boundary
`requires.variables` is the boundary between what a preset carries and what it
refuses to carry. A preset that declares a required variable is saying: *from
here on the configuration is customer-specific; here is what you need and where
the docs are, but you supply the values.* Presets therefore ship no opinionated
RBAC policies, no org-specific catalog rules, and no scaffolder templates —
those belong in your deployment repository. Sample data clearly marked as a
sample (such as the starter tech-radar in `recommended`) is allowed.
---
## Going further
- [Configuration Hierarchy](./configuration-hierarchy.md) — the full
`app-config` merge order and where `app-config.local.yaml` sits.
- [Dynamic Plugins](./dynamic-plugins.md) — how presets enable plugins via the
OCI catalog.
- For per-integration setup and the exact variable meanings, see the
[integrations guides](/devportal/integrations).
---
## Software Templates
## How to Create Components Using Templates in the Developer Portal
This guide provides detailed steps for leveraging the **template creation feature** in the Developer Portal, enabling you to develop components or projects with a predefined structure efficiently.
---
### Overview of the Template Creation Tool
The **template creation feature** helps developers quickly create projects or components by providing a standardized, pre-configured base. This approach reduces the time spent on setup, allowing developers to focus on specific tasks.
Templates include options for linking repositories, integrating databases, and configuring CI/CD pipelines, promoting a seamless workflow from creation to deployment. The specific form fields you see depend entirely on the template selected — each template defines its own parameters.
---
## Step-by-Step Guide
### **Accessing the Tool**
1. Log in to the **Developer Portal**.
2. From the homepage, click the **"Create"** tab in the sidebar menu.
3. Explore the list of project templates to find the one that best fits your needs.
---
### **Registering an Existing Component (Optional)**
If you have a pre-existing component to add:
1. Click **"Register an Existing Component"**.
2. Provide the **URL** of your repository, e.g., GitHub.
3. Link to an entity file such as `catalog-info.yaml`. The wizard will:
- Analyze the file for entities.
- Add valid entities to the DevPortal catalog.
- Suggest a Pull Request for repositories missing the required configuration.
---
### **Choosing a Template**
1. Open the **Templates Page**.
2. Use filters (favorites, tags, names) to search templates.
3. Select a template to view its:
- Description.
- Source code repository link.
- Documentation.
4. Click **"Choose"** to begin configuration.
---
### **Configuring the Template**
Each template defines its own form steps and fields. Common categories of information that templates ask for include:
- **Project information:** name, description, owner.
- **Repository details:** provider (GitHub, GitLab, etc.), organization, repository name, visibility.
- **CI/CD pipeline setup:** target branch, pipeline configuration.
- **Database or infrastructure options:** where applicable, per-template.
The exact fields depend on the template. Review the template's description and documentation before starting.
---
### **Creating the Component**
1. Review all configurations on the **Overview Page**.
2. Ensure all inputs are accurate, then click **"Create"**.
3. Monitor the **creation log** for real-time updates.
Upon completion:
- Access the generated cataloged component.
- Retrieve the source code for further development.
---
## Troubleshooting
- Restart builds or review logs to resolve issues during the process.
---
## Why Templates Matter
Templates are the primary mechanism through which [Golden Paths](/platform/concepts/golden-paths) are delivered in the DevPortal. Rather than a developer searching for the "right" way to start a new service, a well-crafted template makes the right way the easiest way.
- **Self-service:** Teams spin up new services without opening a ticket to the platform team.
- **Standardization:** Every project starts from the same base — same Dockerfile structure, same CI pipeline, same observability hooks.
- **Compliance by default:** Security and governance requirements are built into the template rather than enforced after the fact.
- **Scalability:** Adding a new team or project type means adding a template, not duplicating runbooks.
---
## What template execution actually produces
Running a template creates more than a project skeleton. The scaffolder executes a sequence of steps that leaves a durable trail in the catalog:
1. **A new repository in the configured SCM** — the target provider (GitHub, GitLab, etc.) and organization are set by the SCM preset (`github`, `gitlab`, `azure`) selected via `VEECODE_PRESETS`. The scaffolder uses those credentials; the developer doesn't configure SCM access per template.
2. **A `catalog-info.yaml` committed to that repository** — this is the entity descriptor. It declares the component kind, owner, system, and — critically — the annotations that bind the entity to plugins.
3. **A Location entity registered in the catalog** — the scaffolder registers a `Location` pointing at the new `catalog-info.yaml`. From that moment the new component appears in the portal catalog without any manual registration step.
4. **An entity wired for plugins, RBAC, and TechDocs** — the component is visible immediately, and the tabs and cards that appear on it are determined by what annotations the template wrote into `catalog-info.yaml`.
The last point is the key design decision for template authors. A template that emits no annotations produces a catalog entry with no plugin tabs. A template that emits the right annotations produces a component that already has its CI tab, Kubernetes tab, and code-quality cards populated — before the developer pushes a single commit.
### The annotation decision happens at template authoring time
When a developer runs a template, they don't see annotation fields. Those are fixed by the template. The platform team decides which annotations to include when they write the template, and every component created from that template inherits the same plugin surface.
Example: a template for a Node.js API service might emit:
```yaml
metadata:
annotations:
backstage.io/kubernetes-label-selector: 'app={{ values.componentId }}'
gitlab.com/project-slug: '{{ values.group }}/{{ values.componentId }}'
backstage.io/techdocs-ref: dir:.
```
Every component created from that template will have a Kubernetes tab, a GitLab CI tab, and TechDocs — provided those plugins are loaded and the backends are configured. The developer gets those surfaces automatically; the platform team controls what "automatically" means.
For the full picture of how load, context, and backend interact, see [Composing a Portal](./portal-composition.md).
---
## References
- [Writing Templates](./writing-templates.md) — author your own templates from scratch
- [Composing a Portal](./portal-composition.md) — how plugins attach to entities via the three-layer model
- [The Catalog](./catalog.md) — entity kinds, ownership, and how `catalog-info.yaml` is processed
- [Golden Paths](/platform/concepts/golden-paths) — the platform strategy that templates implement
---
If additional help is needed, contact the support team or watch the video.