Back to Equalify Dashboard

Scanning Services

Edit on GitHub

Scanning Services

Equalify's accessibility scanning is performed by a collection of microservices that work together to scan HTML pages and PDF documents at scale.

How Equalify Scans: URL-Based vs Crawling

Many accessibility tools (such as SiteImprove, Pope Tech, or Monsido) use a crawling approach: you provide a root domain, and the tool automatically discovers pages by following links, parsing sitemaps, and spidering the site. While convenient, crawling has notable drawbacks:

  • Unpredictable scope: Crawlers may miss pages behind JavaScript navigation or follow links to external sites, leading to incomplete or noisy results.
  • Slow discovery: A full crawl of a large site can take hours before scanning even begins.
  • No PDF coverage: Most crawlers focus on HTML pages and ignore linked PDF documents.
  • Redundant scans: Crawlers often re-scan unchanged pages, wasting resources.
Equalify takes a URL-based approach instead. Users provide an explicit list of URLs — either entered manually or uploaded via CSV — and Equalify scans exactly those pages. This design offers several advantages:

Crawling ToolsEqualify (URL-Based)
ScopeAutomatic discovery; may miss or over-include pagesExplicit — you choose exactly what's scanned
PDF supportTypically limited or absentFirst-class PDF scanning via veraPDF
SpeedCrawl + scan (slow for large sites)Scan only (no discovery overhead)
RepeatabilityResults vary based on crawl pathDeterministic — same URLs every time
Gated contentRequires authenticated crawling setupScan any publicly accessible URL directly
This means Equalify does not automatically discover pages on your site. If you add a new page, you need to add its URL to your audit. The trade-off is full control over what gets scanned and consistent, reproducible results across scan runs.

Tip: For large sites, use the CSV upload feature to bulk-add URLs. You can export a URL list from your CMS, sitemap.xml, or analytics tool and import it directly into an Equalify audit.

Overview

The scanning architecture uses AWS SQS FIFO queues to distribute work across scanner Lambdas, ensuring reliable delivery and ordered processing per audit.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐     ┌───────────┐
│   Backend   │────▶│  SQS Router  │────▶│  SQS Queue  │────▶│  Scanner  │
│     API     │     │    Lambda    │     │   (FIFO)    │     │  Lambda   │
└─────────────┘     └──────────────┘     └─────────────┘     └───────────┘
                                                                    │
                                                                    ▼
                                                             ┌───────────┐
                                                             │  Webhook  │
                                                             │ (Backend) │
                                                             └───────────┘

Service Components

SQS Router (aws-lambda-scan-sqs-router)

Purpose: Receives scan requests and routes URLs to appropriate SQS queues based on content type.

Input Schema (Zod validated):

const scansSchema = z.object({
  urls: z.array(
    z.object({
      auditId: z.string(),
      scanId: z.string(),
      urlId: z.string(),
      url: z.string(),
      type: z.string(),      // "html" or "pdf"
      isStaging: z.boolean().optional(),
    })
  ),
});

Behavior:

  • Parse and validate incoming URL list
  • Separate URLs by type (HTML vs PDF)
  • Batch URLs into groups of 10 (SQS limit)
  • Send batches to appropriate FIFO queues
Queue Configuration:
  • HTML Queue: scanHtml.fifo
  • PDF Queue: scanPdf.fifo
  • Message Group ID: auditId (ensures ordered processing per audit)
  • Deduplication ID: urlId (prevents duplicate scans)

HTML Scanner (aws-lambda-scan-html)

Purpose: Scans web pages for accessibility issues using a headless Chromium browser and axe-core.

Key Features:

  • Uses AWS Lambda Powertools for metrics and logging
  • Processes SQS records with partial batch failure handling
  • 2-minute timeout per scan to prevent Lambda hangs
  • Converts axe-core results to EqualifyV2 format
Scan Flow:
  • Receive SQS message with URL details
  • Launch headless Chromium browser
  • Navigate to URL and wait for page load
  • Inject and run axe-core accessibility tests
  • Convert results to EqualifyV2 format
  • POST results to webhook endpoint
Error Handling:
  • Timeout errors: Logged and reported as failures
  • Network errors: Caught and reported with error type
  • Partial failures: Uses AWS Powertools batch processor

PDF Scanner (aws-lambda-scan-pdf)

Purpose: Orchestrates PDF accessibility scanning using veraPDF.

Architecture:

  • TypeScript Lambda receives SQS message
  • Invokes Java-based veraPDF Lambda
  • Converts veraPDF results to EqualifyV2 format
  • Reports results via webhook

veraPDF Interface (aws-lambda-verapdf-interface)

Purpose: Java Lambda that performs actual PDF scanning using veraPDF library.

Features:

  • Native Java Lambda for performance
  • Downloads PDF from URL
  • Executes veraPDF accessibility checks
  • Returns raw JSON results

Result Format (EqualifyV2)

All scanners convert their results to a unified format:

interface StreamResults {
  status: string;       // "complete" or "failed"
  auditId: string;
  scanId: string;
  urlId: string;
  url?: string;
  blockers: Blocker[];
  date: string;
  message: string;
}

interface Blocker {
  source: string;       // "axe-core" | "editoria11y" | "pdf-scan"
  test: string;         // Rule ID (e.g., "color-contrast")
  tags?: string[];      // WCAG tags (e.g., ["wcag2aa", "wcag143"])
  description: string;  // Human-readable rule description
  summary: string;      // Specific failure details
  node: string | null;  // HTML snippet or null for PDFs
}

Axe-Core Conversion

The AxeToEqualify2 converter transforms axe-core results:

function convertToEqualifyV2(axeResult: AxeResults, job: any): StreamResults {
  const blockers: Blocker[] = [];
  
  // Process violations and incomplete results
  [axeResult.incomplete, axeResult.violations].forEach(results => {
    results?.forEach(rule => {
      rule.nodes?.forEach(node => {
        blockers.push({
          source: "axe-core",
          test: rule.id,
          tags: rule.tags,
          description: `${rule.description}. ${rule.help}`,
          summary: node.failureSummary ?? "",
          node: node.html
        });
      });
    });
  });
  
  return { auditId, scanId, urlId, url, blockers, status: "complete", ... };
}

Webhook Integration

Results are sent to the backend API webhook:

EnvironmentEndpoint
Productionhttps://api.equalifyapp.com/public/scanWebhook
Staginghttps://api-staging.equalifyapp.com/public/scanWebhook
Webhook Payload:
{
  "auditId": "uuid",
  "scanId": "uuid",
  "urlId": "uuid",
  "url": "https://example.com",
  "status": "complete",
  "blockers": [...]
}

Metrics & Monitoring

Using AWS Lambda Powertools:

  • scansStarted: Count of scans initiated
  • ScanDuration: Time to complete each scan (milliseconds)
  • Cold start metrics captured automatically

Adding New Scanners

To add a new scanner type:

  • Create a new service in services/
  • Implement SQS message consumption
  • Create a converter in shared/convertors/
  • Add queue routing in aws-lambda-scan-sqs-router
  • Register the new type in the scan schema

For database schema details, see the Database Guide.