Skip to main content
Version: v2 (preview)

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:

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.

Presets replace the V1 VEECODE_PROFILE

In the previous distro model, a single VEECODE_PROFILE selected one app-config.<profile>.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/<name>.yaml that declares three things:

FieldPurpose
requires.variablesThe environment variables the operator must supply, each with a description, required flag, optional docs URL, and example.
pluginsThe dynamic plugins the situation needs, as OCI references (oci://...!<selector>). Each entry's package: must match an entry in dynamic-plugins.default.yaml so the resolver flips it from disabled: true to disabled: false.
appConfigThe Backstage app-config block those plugins expect. Written to its own app-config.preset-<name>.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 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-<name>-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-<name>.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 for the full precedence chain.

The package: value must match exactly

A preset's package: string must match the entry in dynamic-plugins.default.yaml verbatim — including the ${PLUGIN_REGISTRY} and ${BACKSTAGE_VERSION} variable forms. A mismatch installs the plugin a second time under a different name and the backend crashes 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:

# 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). 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.

PresetWhat it enablesRequired env vars
recommendedMarketplace (front + back), pending-changes, tech-radar (sample data), RBAC UInone
veecode-themeVeeCode brand palette + typography + logos/faviconsnone
githubGitHub PAT integration + repo discovery + Actions UI tab. Does not wire OAuth sign-inGITHUB_PAT, GITHUB_ORG
github-authGitHub OAuth sign-in + org/team user sync (identity group)GITHUB_PAT, GITHUB_ORG, GITHUB_AUTH_CLIENT_ID, GITHUB_AUTH_CLIENT_SECRET
gitlabGitLab OAuth sign-in + integration + repo/org catalog discovery (identity group)GITLAB_HOST, GITLAB_AUTH_CLIENT_ID, GITLAB_AUTH_CLIENT_SECRET, GITLAB_TOKEN, GITLAB_GROUP
azureAzure DevOps integration + catalog + pipelines/PR UI. Does not wire Microsoft sign-inAZURE_DEVOPS_TOKEN, AZURE_DEVOPS_HOST, AZURE_DEVOPS_ORG, AZURE_DEVOPS_PROJECT
azure-authMicrosoft (Entra ID) OAuth sign-in + msgraphOrg user sync (identity group)AZURE_AUTH_TENANT_ID, AZURE_AUTH_CLIENT_ID, AZURE_AUTH_CLIENT_SECRET
keycloakKeycloak / OIDC sign-in + keycloakOrg user/group sync (identity group)KEYCLOAK_BASE_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, AUTH_SESSION_SECRET
ldapLDAP 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-adActive Directory overrides for ldap (sAMAccountName, AD object classes). Compose with ldap (compose with ldap; not a standalone identity preset)none (reuses ldap vars)
jenkinsJenkins CI tab on entity pagesJENKINS_URL, JENKINS_USERNAME, JENKINS_TOKEN
kubernetesKubernetes workloads tab on entity pagesK8S_CLUSTER_NAME, K8S_CLUSTER_URL, K8S_CLUSTER_TOKEN
sonarqubeSonarQube code-quality tab + scaffolder actionSONARQUBE_BASE_URL, SONARQUBE_API_KEY
mcpMCP server at /api/mcp-actions/v1 for external AI clients via OAuth/DCRnone
mcp-chatAI 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)

docker run -p 7007:7007 \
-e VEECODE_PRESETS=recommended,veecode-theme \
veecode/devportal:2.0.0

The evaluation starting point: VeeCode brand palette, marketplace, tech-radar, RBAC UI, pending-changes widget.

GitHub-integrated stack

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.0.0

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

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.0.0

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