The Tech Insights plugin provides a comprehensive framework for evaluating and tracking the technical health of your software components. The concept of Tech Insights was part of the original Backstage vision showcased in the first Introduction to Backstage demo video. The open-spource version was originally developed by Roadie and is now maintained as a community plugin, Tech Insights enables engineering teams to define measurable standards and automatically assess compliance across their service catalog.
At its core, the plugin operates on two key concepts: Facts and Checks. Facts are data points about your entities collected from various sources, while Checks are rules that evaluate these facts to determine compliance with your engineering standards. This approach allows you to create automated scorecards that provide visibility into technical debt, security compliance, documentation quality, and operational readiness.
The plugin excels in several key areas:
- Automated compliance tracking - Set up rules to automatically evaluate your services against engineering best practices
 - Technical debt visibility - Identify components that need attention based on configurable criteria
 - Standards enforcement - Encourage adoption of organizational standards through transparent scoring
 - Historical tracking - Monitor improvements over time with built-in data retention and lifecycle management
 - Flexible data sources - Integrate with any external API or service to collect relevant metrics
 
Whether you’re tracking security vulnerabilities, documentation coverage, test automation, or custom organizational standards, Tech Insights provides the foundation for data-driven engineering excellence.
Installation Instructions
These instructions apply to self-hosted Backstage only. To use this plugin on Roadie, visit the docs.
Install the Tech Insights backend plugin
yarn --cwd packages/backend add @backstage-community/plugin-tech-insights-backendInstall the JSON Rules Engine Fact Checker module
yarn --cwd packages/backend add @backstage-community/plugin-tech-insights-backend-module-jsonfcInstall the frontend plugin
yarn --cwd packages/app add @backstage-community/plugin-tech-insightsAdd the backend plugin to your Backstage instance
// packages/backend/src/index.ts
backend.add(import('@backstage-community/plugin-tech-insights-backend'));
backend.add(import('@backstage-community/plugin-tech-insights-backend-module-jsonfc'));Configure fact retrievers in your app-config.yaml
techInsights:
  factRetrievers:
    entityMetadataFactRetriever:
      cadence: '*/15 * * * *'  # Every 15 minutes
      lifecycle: 
        timeToLive: { weeks: 2 }
    entityOwnershipFactRetriever:
      cadence: '0 2 * * *'     # Daily at 2 AM
      lifecycle: 
        maxItems: 10
    techdocsFactRetriever:
      cadence: '0 */6 * * *'   # Every 6 hours
      lifecycle:
        timeToLive: { days: 30 }Configure fact checker rules in your app-config.yaml
techInsights:
  factChecker:
    checks:
      groupOwnerCheck:
        type: json-rules-engine
        name: 'Group Owner Check'
        description: 'Verifies that a component has a group owner'
        factIds:
          - entityOwnershipFactRetriever
        rule:
          conditions:
            all:
              - fact: hasGroupOwner
                operator: equal
                value: true
      titleCheck:
        type: json-rules-engine
        name: 'Entity Title Check'
        description: 'Verifies that entities have descriptive titles'
        factIds:
          - entityMetadataFactRetriever
        rule:
          conditions:
            all:
              - fact: hasTitle
                operator: equal
                value: true
              - fact: hasDescription
                operator: equal
                value: trueAdd the Scorecards page to entity pages
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityTechInsightsScorecardContent } from '@backstage-community/plugin-tech-insights';
const serviceEntityPage = (
  <EntityLayout>
    <EntityLayout.Route path="/tech-insights" title="Scorecards">
      <EntityTechInsightsScorecardContent
        title="Tech Insights"
        description="Track your component's compliance with engineering standards"
      />
    </EntityLayout.Route>
  </EntityLayout>
);Add the global Scorecards page
// packages/app/src/App.tsx
import { TechInsightsScorecardPage } from '@backstage-community/plugin-tech-insights';
<Route path="/tech-insights" element={<TechInsightsScorecardPage />} />Add navigation menu item
// packages/app/src/components/Root/Root.tsx
import EmojiObjectsIcon from '@material-ui/icons/EmojiObjects';
<SidebarItem 
  icon={EmojiObjectsIcon} 
  to="tech-insights" 
  text="Tech Insights" 
/>Things to Know
What are Facts and Fact Retrievers?
Facts are individual data points about entities in your Backstage catalog. A Fact Retriever is responsible for collecting and providing these facts to the Tech Insights system. The plugin comes with three built-in fact retrievers:
- entityMetadataFactRetriever - Extracts facts from entity metadata like title, description, and labels
 - entityOwnershipFactRetriever - Provides ownership information including individual and group owners
 - techdocsFactRetriever - Collects TechDocs related information such as documentation presence
 
Creating Custom Fact Retrievers
You can create custom fact retrievers to collect data from any external system. Here’s an example that checks GitHub repository information:
import { FactRetriever } from '@backstage-community/plugin-tech-insights-node';
import { CatalogClient } from '@backstage/catalog-client';
const githubFactRetriever: FactRetriever = {
  id: 'github-repository-factretriever',
  version: '1.0.0',
  entityFilter: [{ kind: 'component' }],
  schema: {
    hasReadme: {
      type: 'boolean',
      description: 'Repository has a README file',
    },
    hasLicense: {
      type: 'boolean', 
      description: 'Repository has a license file',
    },
    openIssuesCount: {
      type: 'integer',
      description: 'Number of open issues',
    },
    starCount: {
      type: 'integer',
      description: 'Number of GitHub stars',
    },
  },
  handler: async ctx => {
    const { discovery, logger } = ctx;
    const catalogClient = new CatalogClient({ discoveryApi: discovery });
    
    const entities = await catalogClient.getEntities({
      filter: [{ kind: 'component' }],
    });
    const results = await Promise.all(
      entities.items.map(async entity => {
        // Extract GitHub repo URL from annotations
        const repoUrl = entity.metadata.annotations?.['github.com/project-slug'];
        if (!repoUrl) {
          return null;
        }
        // Fetch data from GitHub API
        const repoData = await fetchGitHubRepoData(repoUrl);
        
        return {
          entity: {
            namespace: entity.metadata.namespace,
            kind: entity.kind,
            name: entity.metadata.name,
          },
          facts: {
            hasReadme: repoData.hasReadme,
            hasLicense: repoData.hasLicense,
            openIssuesCount: repoData.openIssues,
            starCount: repoData.stargazersCount,
          },
        };
      })
    );
    return results.filter(Boolean);
  },
};How do I configure Fact Checkers?
Fact Checkers evaluate facts against defined rules to determine compliance. The JSON Rules Engine Fact Checker is the most commonly used implementation, allowing you to define complex rules using JSON configuration.
Basic Check Configuration
techInsights:
  factChecker:
    checks:
      repositoryQualityCheck:
        type: json-rules-engine
        name: 'Repository Quality Standards'
        description: 'Ensures repositories meet basic quality standards'
        factIds:
          - github-repository-factretriever
        rule:
          conditions:
            all:
              - fact: hasReadme
                operator: equal
                value: true
              - fact: hasLicense  
                operator: equal
                value: true
              - fact: openIssuesCount
                operator: lessThan
                value: 50Advanced Rules with Multiple Conditions
You can create sophisticated rules using all, any, and nested conditions:
techInsights:
  factChecker:
    checks:
      productionReadinessCheck:
        type: json-rules-engine
        name: 'Production Readiness'
        description: 'Comprehensive check for production-ready services'
        factIds:
          - entityMetadataFactRetriever
          - entityOwnershipFactRetriever
          - github-repository-factretriever
        rule:
          conditions:
            all:
              - fact: hasTitle
                operator: equal
                value: true
              - fact: hasDescription
                operator: equal
                value: true  
              - fact: hasGroupOwner
                operator: equal
                value: true
              - conditions:
                  any:
                    - fact: hasReadme
                      operator: equal
                      value: true
                    - fact: hasTechDocs
                      operator: equal
                      value: trueAvailable Operators
The JSON Rules Engine supports various operators for different data types:
- Equality: 
equal,notEqual - Numerical: 
lessThan,lessThanInclusive,greaterThan,greaterThanInclusive - Array: 
in,notIn,contains,doesNotContain - String: 
regex(for pattern matching) 
How do I configure lifecycle and data retention?
Tech Insights allows you to configure data retention policies to prevent your database from being overwhelmed with historical fact data. You can configure retention based on either maximum item count or time-to-live.
Time-based Retention
techInsights:
  factRetrievers:
    myFactRetriever:
      cadence: '0 */6 * * *'  # Every 6 hours
      lifecycle:
        timeToLive: { weeks: 4 }  # Keep data for 4 weeksItem Count-based Retention
techInsights:
  factRetrievers:
    myFactRetriever:
      cadence: '0 2 * * *'     # Daily at 2 AM
      lifecycle:
        maxItems: 30           # Keep only the 30 most recent facts per entityCadence Configuration
Cadence uses cron syntax to define when fact retrieval should occur:
'*/15 * * * *'- Every 15 minutes'0 */6 * * *'- Every 6 hours'0 2 * * *'- Daily at 2 AM'0 0 * * 0'- Weekly on Sunday at midnight'0 0 1 * *'- Monthly on the 1st at midnight
How do I create custom operators?
While the built-in operators cover most use cases, you can extend the JSON Rules Engine with custom operators for specialized logic:
import { 
  JsonRulesEngineFactCheckerFactory 
} from '@backstage-community/plugin-tech-insights-backend-module-jsonfc';
// Custom operator to check semantic version compliance  
const customOperators = [
  {
    name: 'semverGreaterThan',
    operator: (factValue: string, jsonValue: string) => {
      return semver.gt(factValue, jsonValue);
    },
  },
  {
    name: 'isValidEmail',
    operator: (factValue: string) => {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(factValue);
    },
  },
];
const factCheckerFactory = new JsonRulesEngineFactCheckerFactory({
  logger: env.logger,
  checks: [...], // your checks
  operators: customOperators,
});How do I configure additional data stores?
By default, Tech Insights uses your Backstage database to store facts. For high-volume fact collection or specialized storage requirements, you can configure additional data stores:
import { 
  DatabaseFactRetrieverRepository,
  DatabaseFactCheckerRepository
} from '@backstage-community/plugin-tech-insights-backend';
// Custom storage implementation
class CustomFactRepository implements FactRepository {
  // Implement required methods for your storage backend
  async insertFacts(facts: Facts[]): Promise<void> {
    // Store facts in your preferred storage system
  }
  
  async getLatestFacts(entity: EntityRef, factIds: string[]): Promise<Facts> {
    // Retrieve latest facts from your storage system  
  }
  
  // ... other required methods
}
// Configure the backend to use your custom repository
const builder = buildTechInsightsContext({
  logger: env.logger,
  config: env.config,
  database: env.database,
  discovery: env.discovery,
  factRepository: new CustomFactRepository(),
  factRetrieverRepository: new CustomFactRetrieverRepository(),
});How do I retrieve and visualize fact data?
Tech Insights exposes REST APIs that allow you to query fact data for custom visualizations and reporting:
Retrieving Latest Facts
curl "https://your-backstage-instance/api/tech-insights/facts/latest?entity=component:default/my-service&ids[]=github-repository-factretriever"Retrieving Historical Data
curl "https://your-backstage-instance/api/tech-insights/facts/range?entity=component:default/my-service&ids[]=github-repository-factretriever&startDatetime=2024-01-01T00:00:00Z&endDatetime=2024-02-01T00:00:00Z"Creating Custom Dashboards
You can build custom React components that consume this API data:
import { techInsightsApiRef } from '@backstage-community/plugin-tech-insights';
import { useApi } from '@backstage/core-plugin-api';
export const CustomTechInsightsDashboard = () => {
  const techInsightsApi = useApi(techInsightsApiRef);
  
  const [facts, setFacts] = useState([]);
  
  useEffect(() => {
    techInsightsApi
      .getFacts('component:default/my-service', ['my-fact-retriever'])
      .then(setFacts);
  }, []);
  
  return (
    <div>
      {/* Render your custom visualization */}
    </div>
  );
};How do I create custom fact retrievers for my own data sources?
Tech Insights uses Fact Retrievers to collect data from any external system or API. The examples above show how to create custom fact retrievers, but here are some additional patterns and best practices:
Integrating with External APIs
const sonarQubeFactRetriever: FactRetriever = {
  id: 'sonarqube-quality-factretriever',
  version: '1.0.0',
  entityFilter: [{ kind: 'component' }],
  schema: {
    codeSmells: { type: 'integer', description: 'Number of code smells' },
    coverage: { type: 'number', description: 'Test coverage percentage' },
    securityHotspots: { type: 'integer', description: 'Security hotspots count' },
    qualityGate: { type: 'string', description: 'Quality gate status' },
  },
  handler: async ctx => {
    const { logger } = ctx;
    const catalogClient = new CatalogClient({ discoveryApi: ctx.discovery });
    
    const entities = await catalogClient.getEntities({
      filter: [{ kind: 'component' }],
    });
    return Promise.all(entities.items.map(async entity => {
      const sonarProject = entity.metadata.annotations?.['sonarqube.org/project-key'];
      if (!sonarProject) return null;
      try {
        const metrics = await fetchSonarQubeMetrics(sonarProject);
        return {
          entity: {
            namespace: entity.metadata.namespace,
            kind: entity.kind,
            name: entity.metadata.name,
          },
          facts: {
            codeSmells: metrics.code_smells,
            coverage: parseFloat(metrics.coverage),
            securityHotspots: metrics.security_hotspots,
            qualityGate: metrics.alert_status,
          },
        };
      } catch (error) {
        logger.warn(`Failed to fetch SonarQube data for ${entity.metadata.name}: ${error}`);
        return null;
      }
    })).then(results => results.filter(Boolean));
  },
};Error Handling and Resilience
Always implement proper error handling in your fact retrievers to ensure system stability:
handler: async ctx => {
  const { logger } = ctx;
  const results = [];
  try {
    // Your fact collection logic
    const entities = await catalogClient.getEntities({
      filter: [{ kind: 'component' }],
    });
    for (const entity of entities.items) {
      try {
        const facts = await collectFactsForEntity(entity);
        results.push({ entity: entityRef(entity), facts });
      } catch (entityError) {
        logger.warn(`Failed to collect facts for ${entity.metadata.name}:`, entityError);
        // Continue processing other entities
      }
    }
  } catch (criticalError) {
    logger.error('Critical error in fact retriever:', criticalError);
    throw criticalError;
  }
  return results;
},How do I create non-boolean checks?
While many checks result in pass/fail (boolean) outcomes, you can create checks that evaluate to different result types by implementing custom FactCheckerFactory classes:
import { FactCheckerFactory, FactChecker } from '@backstage-community/plugin-tech-insights-node';
class ScoreBasedFactCheckerFactory implements FactCheckerFactory {
  async createFactChecker(): Promise<FactChecker> {
    return {
      async runChecks(entity, facts) {
        const checks = [
          {
            id: 'code-quality-score',
            type: 'score-based',
            name: 'Code Quality Score',
            description: 'Composite score based on multiple metrics',
            result: this.calculateQualityScore(facts),
            facts,
          }
        ];
        
        return { entity, checks };
      }
    };
  }
  private calculateQualityScore(facts: any): { score: number; level: string } {
    let score = 100;
    
    // Deduct points for code smells
    score -= Math.min(facts.codeSmells * 2, 30);
    
    // Deduct points for low coverage
    if (facts.coverage < 80) {
      score -= (80 - facts.coverage);
    }
    
    // Determine quality level
    let level = 'excellent';
    if (score < 90) level = 'good';
    if (score < 70) level = 'needs-improvement';
    if (score < 50) level = 'poor';
    
    return { score, level };
  }
}Frontend Customization and Visualization Options
The Tech Insights plugin provides several visualization components that you can customize:
Scorecard Views
// Badge view for compact display
<EntityTechInsightsScorecardContent 
  title="Quick Status"
  description="At-a-glance compliance status"
  checkType="badge"
/>
// Gauge view for score-based metrics  
<EntityTechInsightsScorecardContent
  title="Quality Metrics" 
  description="Detailed quality measurements"
  checkType="gauge"
/>
// Linear view for detailed breakdowns
<EntityTechInsightsScorecardContent
  title="Compliance Checklist"
  description="Complete compliance status" 
  checkType="linear"
/>Filtering and Customization
<EntityTechInsightsScorecardContent
  title="Security Compliance"
  description="Security-focused checks only"
  checksId="security.*"           // Regex pattern to filter checks
  showOnlyFailedChecks={true}     // Show only failing checks
  showDescription={false}         // Hide check descriptions
/>How do I manage database performance?
For production deployments with high-volume fact collection, consider these performance optimizations:
Database Indexing
Ensure your database has appropriate indexes for Tech Insights queries:
-- Indexes for common query patterns
CREATE INDEX idx_ti_facts_entity ON ti_facts(entity_id);
CREATE INDEX idx_ti_facts_timestamp ON ti_facts(timestamp);
CREATE INDEX idx_ti_facts_fact_retriever ON ti_facts(fact_retriever_id);
CREATE INDEX idx_ti_facts_composite ON ti_facts(entity_id, fact_retriever_id, timestamp);Batch Processing
For fact retrievers that process many entities, consider batch processing:
handler: async ctx => {
  const entities = await catalogClient.getEntities({
    filter: [{ kind: 'component' }],
  });
  // Process in batches of 50
  const batchSize = 50;
  const results = [];
  
  for (let i = 0; i < entities.items.length; i += batchSize) {
    const batch = entities.items.slice(i, i + batchSize);
    const batchResults = await Promise.allSettled(
      batch.map(entity => collectFactsForEntity(entity))
    );
    
    results.push(...batchResults
      .filter(result => result.status === 'fulfilled')
      .map(result => result.value)
    );
  }
  return results;
},More information
For detailed technical documentation and advanced configuration options, refer to these resources:
Changelog
The Tech Insights plugin has not seen any significant changes since a month ago.
Set up Backstage in minutes with Roadie
Focus on using Backstage, rather than building and maintaining it.