# 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.
--- ## Writing Templates This guide covers how to **author** a Backstage software template — the YAML entity that drives the scaffolder wizard. It assumes you know how to run a template as a developer. If you're looking to use existing templates, see [Software Templates](./software-template). --- ## What a template is A template is a catalog entity of `kind: Template`. When the scaffolder backend loads it: 1. It renders a form from `spec.parameters` 2. It executes a sequence of steps from `spec.steps` 3. It shows links and text from `spec.output` The template YAML lives in a Git repository. You register it by pointing a `catalog.locations` entry in `app-config.yaml` at it. --- ## Registering a template Add a location entry to your `app-config.yaml` (or any config layer that is loaded at startup): ```yaml catalog: locations: - type: url target: https://github.com/my-org/my-templates/blob/main/template.yaml rules: - allow: [Template] ``` For files inside the container (e.g., baked into the image at `/app/examples/`): ```yaml catalog: locations: - type: file target: /app/examples/my-template/template.yaml rules: - allow: [Template] ``` --- ## Anatomy: the three sections Every template shares the same top-level structure: ```yaml apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: my-template # unique ID — used in URLs and entity refs title: My Template # display name shown in the template catalog description: Does X # one-line summary shown in the catalog card tags: [github, nodejs] # used for filtering in the UI spec: owner: group:default/platform-team type: service # category label (service, website, library, etc.) parameters: [] # defines the wizard form steps: [] # defines what runs when the user clicks Create output: {} # defines the links and text shown after completion ``` Source: [Backstage — Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates) --- ## Parameters `spec.parameters` is an array. Each item in the array becomes one page in the multi-step wizard. ### Basic types ```yaml parameters: - title: About your service required: - name - owner properties: name: title: Service name type: string description: Unique name — used for the repo and catalog entry ui:autofocus: true owner: title: Owner type: string ui:field: OwnerPicker ui:options: catalogFilter: kind: [Group, User] replicas: title: Replica count type: integer default: 2 enableCache: title: Enable cache? type: boolean default: false ``` Supported types: `string`, `integer`, `number`, `boolean`, `array`, `object`. ### Special UI fields These fields render specialized widgets instead of plain text inputs: | `ui:field` | What it renders | Key `ui:options` | |---|---|---| | `RepoUrlPicker` | Git repo selector (provider + org + repo name) | `allowedHosts`, `allowedOwners` | | `OwnerPicker` | Catalog entity picker pre-filtered to owners | `catalogFilter` | | `EntityPicker` | Any catalog entity picker | `catalogFilter`, `allowArbitraryValues` | ```yaml repoUrl: title: Repository location type: string ui:field: RepoUrlPicker ui:options: allowedHosts: - github.com allowedOwners: - my-org ``` Source: [Backstage — Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates) ### Enum with friendly labels ```yaml environment: title: Target environment type: string enum: [dev, staging, prod] enumNames: ['Development', 'Staging', 'Production'] default: dev ``` ### Conditional fields Use JSON Schema `dependencies` + `allOf` + `if/then` to show or hide fields based on another field's value: ```yaml parameters: - title: Configuration properties: needsDatabase: title: Needs a database? type: boolean default: false dependencies: needsDatabase: allOf: - if: properties: needsDatabase: const: true then: required: - dbName properties: dbName: title: Database name type: string ``` Source: [Backstage — Input examples](https://backstage.io/docs/features/software-templates/input-examples) --- ## Steps `spec.steps` is an array of action invocations executed in order. ### Basic step ```yaml steps: - id: fetch-base # used to reference this step's output in later steps name: Fetch skeleton # display name shown in the execution log action: fetch:template # action ID — see Available Actions input: url: ./content # path to skeleton directory inside this template's repo values: name: ${{ parameters.name }} ``` ### Referencing parameters and step outputs ```yaml # Inject a parameter value into a step input input: description: This is ${{ parameters.name }} # Reference a previous step's output # Use bracket notation when the step ID contains a dash input: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} # Shorthand when the step ID has no dashes input: ref: ${{ steps.register.output.entityRef }} ``` Source: [Backstage — Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates) ### Conditional execution A step only runs when its `if:` expression evaluates to truthy: ```yaml - id: publish-github name: Publish to GitHub if: ${{ parameters.provider === 'github' }} action: publish:github input: repoUrl: ${{ parameters.repoUrl }} - id: register name: Register in catalog if: ${{ parameters.provider !== 'local' }} action: catalog:register input: repoContentsUrl: ${{ steps['publish-github'].output.repoContentsUrl }} catalogInfoPath: /catalog-info.yaml ``` The `if:` field accepts any expression using `===`, `!==`, `!`, `and`, `or`, and `${{ parameters.* }}` or `${{ steps.*.output.* }}` references. Source: [Backstage — Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates) ### Iteration with `each:` Repeat a step for each item in an array: ```yaml - id: fetch-per-env name: Fetch config per environment each: ${{ parameters.environments }} action: fetch:plain:file input: url: ./configs/${{ each.value }}.yaml targetPath: config/${{ each.value }}.yaml ``` For arrays of objects, use `${{ each.value.fieldName }}`: ```yaml - id: process-services each: ${{ parameters.services }} action: fetch:plain:file input: url: ./templates/${{ each.value.language }}.yaml targetPath: services/${{ each.value.name }}.yaml ``` Source: [Backstage — Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates) --- ## Output `spec.output` defines the links and text shown after all steps complete: ```yaml output: links: - title: Repository url: ${{ steps['publish'].output.remoteUrl }} - title: Open in catalog icon: catalog entityRef: ${{ steps['register'].output.entityRef }} - if: ${{ parameters.provider === 'github' }} title: GitHub Actions url: ${{ steps['publish'].output.remoteUrl }}/actions text: - title: Next steps content: | Your service is live. Push your first commit to trigger the CI pipeline. ``` --- ## Complete example A template that creates a Node.js service on GitHub, registers it in the catalog, and notifies the owner. Based directly on the [template-nodejs example](https://github.com/veecode-platform/devportal-base/blob/main/examples/template-nodejs/template.yaml) that ships with VeeCode DevPortal. ```yaml apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: example-nodejs-template title: Node.js Service description: Creates a Node.js repo on GitHub and registers it in the catalog tags: [github, nodejs] spec: owner: group:default/platform-team type: service parameters: - title: About your service required: - name properties: name: title: Name type: string description: Unique name of the component ui:autofocus: true - title: Repository location required: - repoUrl properties: repoUrl: title: Repository location type: string ui:field: RepoUrlPicker ui:options: allowedHosts: - github.com steps: # 1. Copy the skeleton files from ./content in this template's repo, # substituting ${{ values.name }} throughout file contents and paths. - id: fetch-base name: Fetch skeleton action: fetch:template input: url: ./content values: name: ${{ parameters.name }} # 2. Create the GitHub repo and push the workspace content. # The output.repoContentsUrl and output.remoteUrl are used by later steps. - id: publish name: Publish to GitHub action: publish:github input: description: This is ${{ parameters.name }} repoUrl: ${{ parameters.repoUrl }} defaultBranch: main # 3. Register a Location entity pointing at the new catalog-info.yaml, # making the component immediately visible in the catalog. - id: register name: Register in catalog action: catalog:register input: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} catalogInfoPath: /catalog-info.yaml # 4. Send a notification to the user:default/guest entity. # Change entityRefs to the actual owner entity ref in your org. - id: notify name: Notify action: notification:send input: recipients: entity entityRefs: - user:default/guest title: Template executed info: Your template has been executed severity: normal output: links: - title: Repository url: ${{ steps['publish'].output.remoteUrl }} - title: Open in catalog icon: catalog entityRef: ${{ steps['register'].output.entityRef }} ``` To add more integrations, insert steps between `register` and `notify`. Each new step can use `${{ steps['publish'].output.remoteUrl }}` or `${{ parameters.* }}` as inputs. See [Available Actions](./available-actions) for the full list. --- ## References - [Backstage: Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates) — upstream canonical reference - [Backstage: Input examples](https://backstage.io/docs/features/software-templates/input-examples) — parameter patterns and conditional fields - [Available Actions](./available-actions) — all pre-registered actions in VeeCode - [Custom Action](../plugins/development/custom-action) — write your own action in TypeScript when nothing in the list fits - [Software Templates](./software-template) — user guide for running templates --- ## Simple branding There is an entire branding section in the `appConfig` section of DevPortal configuration that lets you pick one of the pre-defined theme variants and its light/dark options, allowing you top customize every single aspect from it. ## The default config The default branding values are defined in `app-config.yaml` (base layer). Override them in `app-config.local.yaml` (mounted at `/app/app-config.local.yaml`): ```yaml app: branding: fullLogo: https://veecode-platform.github.io/support/logos/logo.svg iconLogo: https://veecode-platform.github.io/support/logos/logo-mobile.png fullLogoWidth: 150 theme: light: variant: "backstage" palette: navigation: background: "#222222" dark: variant: "backstage" palette: navigation: background: "#222222" ``` :::note About `fullLogoDark` `fullLogoDark` is read by `CompanyLogo.tsx` and applied as the logo when the dark theme is active. Set it to a separate URL if you want distinct light and dark logos. The "new form breaks sidebar, check later" note in the source applies to a commented-out object form of `fullLogo` (a light/dark map), not to `fullLogoDark`. ::: Will result in this: ![Default branding](/img/assets/default-branding.png) A few points to remember: - You can provide URLs to your custom resources, but remember those must be available to end users' browsers. - You must add the base URL of those resources to the `csp.img-src` list. ## External domains and CSP DevPortal ships with a Content-Security-Policy that restricts which domains can serve images (and other resource types) to the browser. If your logo, icon, or favicon URL is on a domain not already allowed, the browser blocks it — the image silently falls back to default, and the console shows a CSP violation. Extend `backend.csp.img-src` in your `app-config.local.yaml` to add your domain: ```yaml backend: csp: img-src: - "'self'" - "data:" - "https://your-cdn.example.com" # ...plus every domain your instance's current effective config already allows ``` :::warning Config overrides replace arrays, they don't merge them Backstage's config loader replaces array-valued config on override rather than appending to it. If you set `backend.csp.img-src` without including everything already in the default list, you don't just fail to add your domain — you break every other domain that was previously allowed (analytics, GitHub avatars, etc.). Read your instance's current effective `img-src` list before overriding it, and repeat it in full alongside your addition. The same rule applies to `connect-src`, `script-src`, `style-src`, and any other CSP directive. ::: If you're embedding an external tool via `