Skip to main content

Backstage App-Config Structure and Plugin Configuration Patterns Analysis

Analysis Date: 2025-11-19 Project: repo-devex-backstage Analyzed by: Hive Mind Config Analysis Agent


Executive Summary​

This analysis reviews the Backstage configuration structure and existing plugin patterns to ensure consistency for the Terraform State Backend plugin configuration.

Key Findings​

  1. Multi-tier Configuration Architecture: Base config (app-config.yaml) + environment-specific overlays
  2. Environment Variable Pattern: ${VAR_NAME} with optional defaults ${VAR_NAME:-default}
  3. Nested Provider Configuration: catalog.providers.<pluginName> for entity providers
  4. Vault Integration Pattern: Plugins store sensitive tokens in Vault, not config files
  5. Schedule Configuration: Consistent structure with frequency, timeout, initialDelay

Configuration File Structure​

1. Configuration Files Hierarchy​

backstage/
├── app-config.yaml # Base configuration (minimal, shared)
├── app-config.local.yaml # Local development (with secrets)
├── app-config.production.yaml # Production overrides (env vars only)
└── plugins/terraform-state-backend/dev/
└── app-config.local.yaml # Plugin-specific local dev config

Pattern: Base config is minimal, environment-specific configs override and extend.

2. Configuration Sections​

# Standard Backstage sections
app: # App metadata (title, baseUrl)
organization: # Organization name
backend: # Backend server config (baseUrl, database, auth)
auth: # Authentication providers
integrations: # External integrations (GitHub, etc.)
catalog: # Catalog configuration
import: # Import settings
rules: # Entity type rules
locations: # Static catalog locations
providers: # Dynamic entity providers ← PLUGIN CONFIGS HERE
permission: # Permission framework
scaffolder: # Software templates
techdocs: # Documentation
kubernetes: # Kubernetes integration (optional)

# Custom plugin sections
vault: # Vault secrets backend config
claude-flow: # Claude Flow plugin config

Environment Variable Patterns​

Convention Analysis​

Format: ${ENVIRONMENT_VARIABLE_NAME} or ${VAR_NAME:-default_value}

Common Prefixes:

  • AUTH_* - Authentication credentials
  • POSTGRES_* - Database configuration
  • BACKEND_* - Backend server settings
  • APP_* - Frontend application settings
  • VAULT_* - Vault configuration
  • TECHDOCS_* - TechDocs configuration
  • GCP_* - Google Cloud Platform settings

Examples from Production Config:

backend:
baseUrl: ${BACKEND_BASE_URL}
database:
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}

auth:
providers:
github:
production:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

Examples with Defaults (Local Config):

backend:
auth:
keys:
- secret: ${BACKEND_SECRET:-h6ldBXk8U4lCCxkgmccBVZrhYJC/o9xx}

auth:
providers:
google:
development:
clientId: ${AUTH_GOOGLE_CLIENT_ID:-placeholder-client-id}

Catalog Provider Configuration Pattern​

Standard Structure​

All entity providers follow this pattern under catalog.providers.<pluginName>:

catalog:
providers:
<providerName>:
<providerId>: # Unique identifier for this provider instance
# Provider-specific configuration
# ...

# Standard fields across all providers
schedule:
frequency: { minutes: N }
timeout: { minutes: N }
initialDelay: { seconds: N }

Existing Examples​

1. GitHub Organization Provider​

catalog:
providers:
githubOrg:
id: production
githubUrl: https://github.com
orgs: ['badal-io']
schedule:
frequency: { minutes: 10 }
timeout: { minutes: 3 }
initialDelay: { seconds: 30 }

2. GCP Projects Provider​

catalog:
providers:
gcp:
gcpProjects:
organization: 'foundations.badal.io'
schedule:
frequency: { minutes: 15 }
timeout: { minutes: 5 }
initialDelay: { seconds: 60 }

3. GitHub Catalog Provider​

catalog:
providers:
github:
providerId:
catalogPath: /catalog-info.yaml
organization: badal-io
schedule:
frequency: { minutes: 5 }
initialDelay: { seconds: 15 }
timeout: { minutes: 3 }

Terraform State Plugin Configuration Analysis​

Current Plugin Implementation​

Plugin ID: terraform-state Provider Types:

  • GcsStateProvider - Google Cloud Storage
  • TfcStateProvider - Terraform Cloud

Config Path Expected: catalog.providers.terraformState

Provider Configuration Schema​

GCS Provider Config Schema​

interface GcsStateSourceConfig {
id: string; // Provider instance ID
bucketName: string; // GCS bucket name
projectId?: string; // GCP project ID (optional, for ADC)
prefix?: string; // Bucket prefix filter
environment: 'non-production' | 'production';
credentialsFile?: string; // Path to credentials.json (dev only)
schedule?: {
frequency: { minutes: number };
timeout: { minutes: number };
};
}

TFC Provider Config Schema​

interface TfcStateSourceConfig {
id: string; // Provider instance ID
organization: string; // Terraform Cloud organization
workspacePrefix?: string; // Workspace filter
environment: 'non-production' | 'production';
token: {
value?: string; // Direct token (dev only)
vaultPath?: string; // Vault path for token
};
schedule?: {
frequency: { minutes: number };
timeout: { minutes: number };
};
rateLimit?: {
maxConcurrent: number;
retryAfterMs: number;
maxRetries: number;
};
}

Based on existing patterns, the plugin should use this structure:

catalog:
providers:
terraformState:
# GCS provider instances
gcs-nonprod:
type: gcs # Provider type discriminator
bucketName: terraform-state-nonprod
projectId: ${GCP_PROJECT_ID} # Environment variable pattern
prefix: 'backstage/'
environment: non-production
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 5 }
initialDelay: { seconds: 60 }
sensitivityLevel: moderate

gcs-prod:
type: gcs
bucketName: terraform-state-prod
projectId: ${GCP_PROJECT_ID}
prefix: 'backstage/'
environment: production
schedule:
frequency: { minutes: 60 }
timeout: { minutes: 10 }
initialDelay: { seconds: 120 }
sensitivityLevel: strict

# Terraform Cloud provider instances
tfc-nonprod:
type: tfc # Provider type discriminator
organization: Badal_devex
workspacePrefix: 'wrkspc-np-'
environment: non-production
vaultPath: /backstage/backend/data/terraform-state-plugin/tfc-token#Badal_devex
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 5 }
initialDelay: { seconds: 60 }
sensitivityLevel: moderate
rateLimit:
maxConcurrent: 25
retryAfterMs: 2000
maxRetries: 3

Vault Integration Pattern​

How Other Plugins Use Vault​

1. Vault Configuration Section​

vault:
address: https://localhost:8200 # Vault server URL
skipTLS: true # Skip TLS verification (dev only)
paths: # Standardized secret paths
userSecrets: /backstage/users
groupSecrets: /backstage/groups
backendSecrets: /backstage/backend

2. Vault Secrets Backend Plugin Pattern​

The vault-secrets-backend plugin:

  • Reads config from vault.address, vault.skipTLS, vault.paths.*
  • Authenticates using environment variables or ADC
  • Stores user/group/backend secrets in hierarchical paths

3. Terraform Cloud Provider Vault Pattern​

The TFC provider expects:

  • Vault Path: /backstage/backend/data/terraform-state-plugin/tfc-token#<organization>
  • Token Retrieval: Via VaultTokenService class
  • Fallback: Direct token value in config (dev only)
// From TfcStateProvider.ts
if (config.token.vaultPath || !config.token.value) {
this.vaultTokenService = new VaultTokenService(backstageConfig, logger);
}

this.tfcClient = new TfcClient(
{
organization: config.organization,
token: {
value: config.token.value, // Direct value (dev)
vaultPath: config.token.vaultPath, // Vault path (prod)
},
},
this.vaultTokenService, // Token provider
);

Vault Path Convention: /backstage/backend/data/<plugin-id>/<secret-type>#<identifier>

Example:

  • Plugin ID: terraform-state-plugin
  • Secret Type: tfc-token
  • Organization: Badal_devex
  • Full Path: /backstage/backend/data/terraform-state-plugin/tfc-token#Badal_devex

Security Patterns​

1. Secrets Management​

NEVER in Config Files (production):

  • API tokens
  • Passwords
  • Private keys
  • Service account credentials

Environment Variables (with defaults for dev):

clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET:-placeholder-client-secret}

Vault (production):

vaultPath: /backstage/backend/data/plugin-name/secret-type#identifier

Credential Files (local dev only):

credentialsFile: /path/to/service-account.json  # Local dev only

2. GitHub Integration Pattern​

GitHub secrets in local config:

integrations:
github:
- host: github.com
apps:
- appId: 1644052
clientId: Iv23liJiVwdeMmoRoLXM
clientSecret: 7b2d3d540c1111928c48f70b274c9073898e0005
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
[REDACTED - DEMO KEY ONLY]
-----END RSA PRIVATE KEY-----

Production: Environment variables only:

auth:
providers:
github:
production:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

Plugin Registration Pattern​

Backend Plugin Registration​

File: backstage/packages/backend/src/index.ts

Pattern:

import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// Plugin registration
backend.add(import('@internal/plugin-terraform-state-backend'));

All Registered Plugins:

// Core Backstage
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-permission-backend'));
backend.add(import('@backstage/plugin-search-backend'));

// Entity providers (catalog modules)
backend.add(import('@backstage/plugin-catalog-backend-module-github-org'));

// Custom internal plugins
backend.add(import('@internal/plugin-terraform-cloud-backend'));
backend.add(import('@internal/backstage-plugin-vault-secrets-backend'));
backend.add(import('@internal/plugin-claude-flow-backend'));
backend.add(import('@internal/plugin-terraform-state-backend'));

Entity Provider Registration: Providers are automatically discovered via fromConfig() static factory method.

Plugin Structure Pattern​

Standard Backend Plugin:

import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';

export const myPlugin = createBackendPlugin({
pluginId: 'my-plugin-id',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
config: coreServices.rootConfig,
httpRouter: coreServices.httpRouter,
// ... other services
},
async init({ logger, config, httpRouter }) {
// Plugin initialization
logger.info('My plugin loaded');

// Optional: Register HTTP router
httpRouter.use(await createRouter({ config, logger }));
},
});
},
});

Entity Provider Plugin (like Terraform State):

export const terraformStatePlugin = createBackendPlugin({
pluginId: 'terraform-state',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
},
async init({ logger }) {
logger.info('Terraform State plugin loaded');
logger.info('Entity providers must be registered manually in backend/src/index.ts');
},
});
},
});

Note: Entity providers are NOT registered in the plugin itself. They must be manually registered in backend/src/index.ts using the catalog extension point.


1. Configuration Naming Conventions​

Environment Variables:

  • TERRAFORM_STATE_GCS_BUCKET_<ENV> - GCS bucket names
  • TERRAFORM_STATE_GCP_PROJECT - GCP project ID
  • VAULT_TERRAFORM_CLOUD_TOKEN_PATH - Vault path for TFC tokens

Config Keys:

  • catalog.providers.terraformState - Root config path
  • Type discriminator: type: gcs or type: tfc
  • Instance ID: descriptive name (e.g., gcs-nonprod, tfc-prod)

2. Multi-Environment Pattern​

app-config.yaml (base):

# Minimal - no provider configs (too environment-specific)
catalog:
rules:
- allow: [Component, System, API, Resource, Location]

app-config.local.yaml (development):

catalog:
providers:
terraformState:
gcs-local-emulator:
type: gcs
bucketName: terraform-state-local
apiEndpoint: http://localhost:4443 # GCS emulator
projectId: local-dev-project
prefix: 'test/'
environment: non-production
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 2 }

tfc-dev:
type: tfc
organization: Badal_devex
workspacePrefix: 'test-'
environment: non-production
token:
value: ${TERRAFORM_CLOUD_TOKEN:-placeholder} # Fallback for dev
schedule:
frequency: { minutes: 10 }
timeout: { minutes: 2 }

app-config.production.yaml (production):

catalog:
providers:
terraformState:
gcs-prod:
type: gcs
bucketName: ${TERRAFORM_STATE_GCS_BUCKET_PROD}
projectId: ${GCP_PROJECT_ID}
prefix: 'production/'
environment: production
schedule:
frequency: { minutes: 60 }
timeout: { minutes: 10 }
initialDelay: { seconds: 120 }
sensitivityLevel: strict

tfc-prod:
type: tfc
organization: ${TERRAFORM_CLOUD_ORGANIZATION}
workspacePrefix: ${TERRAFORM_CLOUD_WORKSPACE_PREFIX}
environment: production
vaultPath: /backstage/backend/data/terraform-state-plugin/tfc-token#${TERRAFORM_CLOUD_ORGANIZATION}
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 5 }
initialDelay: { seconds: 60 }
sensitivityLevel: strict

3. Security Best Practices​

Development:

  • Use emulators (GCS emulator, local Vault)
  • Hardcode test credentials in app-config.local.yaml
  • Add app-config.local.yaml to .gitignore

Production:

  • ALL credentials via environment variables
  • Sensitive tokens in Vault
  • Use ${VAR_NAME} without defaults for required secrets
  • Use Application Default Credentials (ADC) for GCP

Configuration Validation​

Type Safety​

The provider config is validated by TypeScript interfaces:

// GCS validation
const providerConfig: GcsStateSourceConfig = {
id: `gcs-${id}`,
bucketName: sourceConfig.bucketName, // Required
projectId: sourceConfig.projectId, // Optional
prefix: sourceConfig.prefix, // Optional
environment: sourceConfig.environment || 'non-production',
credentialsFile: sourceConfig.credentialsFile,
schedule: sourceConfig.schedule,
};

// TFC validation
const providerConfig: TfcStateSourceConfig = {
id: `tfc-${id}`,
organization: sourceConfig.organization, // Required
workspacePrefix: sourceConfig.workspacePrefix,
environment: sourceConfig.environment || 'non-production',
token: {
value: sourceConfig.token?.value,
vaultPath: sourceConfig.token?.vaultPath,
},
schedule: sourceConfig.schedule,
rateLimit: sourceConfig.rateLimit,
};

Runtime Validation​

Provider factory methods validate config:

static fromConfig(config: Config, options): Provider[] {
const configs = config.getOptionalConfig('catalog.providers.terraformState');

if (!configs) {
return []; // No providers configured
}

for (const [id, sourceConfig] of Object.entries(configs.get())) {
// Validate required fields
if (!sourceConfig.bucketName) {
throw new Error(`bucketName required for GCS provider ${id}`);
}

providers.push(new GcsStateProvider(providerConfig, logger, schedule));
}

return providers;
}

Recommendations​

1. Configuration Structure​

Use nested provider structure:

catalog:
providers:
terraformState:
<provider-id>:
type: gcs | tfc
# ... provider-specific config

Provider ID naming: <type>-<environment> (e.g., gcs-nonprod, tfc-prod)

2. Environment Variables​

Define standard variables:

  • GCP_PROJECT_ID - GCP project for GCS buckets
  • TERRAFORM_CLOUD_ORGANIZATION - TFC organization name
  • TERRAFORM_CLOUD_WORKSPACE_PREFIX - Workspace filter

Vault paths:

  • /backstage/backend/data/terraform-state-plugin/tfc-token#<organization>

3. Schedule Configuration​

Follow existing pattern:

schedule:
frequency: { minutes: N }
timeout: { minutes: N }
initialDelay: { seconds: N }

Recommended intervals:

  • Development: 5-10 minutes
  • Non-production: 15-30 minutes
  • Production: 30-60 minutes

4. Multi-Instance Support​

Support multiple provider instances for:

  • Different environments (nonprod/prod)
  • Different organizations (multiple TFC orgs)
  • Different buckets (regional separation)

Example:

catalog:
providers:
terraformState:
gcs-us-nonprod:
type: gcs
bucketName: terraform-state-us-nonprod
# ...

gcs-eu-nonprod:
type: gcs
bucketName: terraform-state-eu-nonprod
# ...

tfc-devex:
type: tfc
organization: Badal_devex
# ...

tfc-platform:
type: tfc
organization: Badal_platform
# ...

5. Type Discriminator​

Add type field to distinguish GCS vs TFC providers:

terraformState:
instance-id:
type: gcs # or 'tfc'

This allows parsing both provider types from the same config section.


Summary​

Naming Conventions​

PatternExample
Environment variables${POSTGRES_HOST}, ${AUTH_GITHUB_CLIENT_ID}
Config pathscatalog.providers.terraformState
Provider IDsgcs-nonprod, tfc-prod
Vault paths/backstage/backend/data/<plugin>/<secret>#<id>

Configuration Hierarchy​

  1. app-config.yaml - Base, minimal shared config
  2. app-config.local.yaml - Local dev with hardcoded secrets
  3. app-config.production.yaml - Production with env vars only

Security Patterns​

  • Local dev: Hardcoded credentials in app-config.local.yaml (gitignored)
  • Production: Environment variables + Vault for tokens
  • GCP Auth: Application Default Credentials (ADC)
  • TFC Auth: Vault token storage

Provider Registration​

  • Backend plugin registered in backend/src/index.ts via backend.add()
  • Entity providers auto-discovered via fromConfig() static factory
  • Config path: catalog.providers.<pluginId>

Next Steps for Coder Agent​

  1. Update Provider Factory: Add type discriminator handling
  2. Environment Variables: Document required env vars in README
  3. Vault Integration: Test VaultTokenService for TFC tokens
  4. Multi-Instance: Support multiple GCS/TFC provider instances
  5. Config Validation: Add runtime validation for required fields
  6. Documentation: Create config examples for each environment

Analysis Complete - Ready for implementation phase.