Skip to content

Latest commit

 

History

352 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

a11yengine

License: AGPL v3 PHP Laravel Laravel Forge Site Deployment Status

A web accessibility auditing and risk management platform. It automatically crawls digital properties, runs WCAG and Lighthouse audits, tracks violations as managed issues, generates AI-powered governance reports, risk advisories, content audits, and per-issue remediation guidance — and routes notifications to any email address or Slack/Teams/Discord webhook — all within a strict multi-tenant agency hierarchy.


Features

  • Automated Crawling & Scanning — Discovers all pages and PDF documents on a domain via headless Playwright and runs axe-core (WCAG 2.0/2.1 A/AA) and Lighthouse audits on each page
  • Screen Reader Simulation — A virtual screen reader runner (screenReaderRunner.js) executes 40 deterministic WCAG checks per page covering landmarks, headings, form labelling, focus management, live regions, tables, and language attributes; violations flow through the same Finding + Issue pipeline as axe-core results using sr- prefixed rule keys; toggleable via SCREEN_READER_ENABLED
  • Content Quality Checks — A deterministic content runner (contentRunner.js) executes 14 per-page checks covering image alt quality, link text quality, heading conventions, generic page titles, and video/audio caption and transcript availability (HTML5, YouTube, Vimeo, and other embed services); violations use content- prefixed rule keys and flow through the same Finding + Issue pipeline; toggleable via CONTENT_ENABLED
  • Keyboard Navigation Testing — A deterministic keyboard runner (keyboardRunner.js) executes 7 per-page checks covering positive tabindex usage, non-interactive focusable elements, onclick-without-keyboard handlers, autofocus misuse, offscreen-focusable elements, aria-disabled inconsistencies, and composite widget roving-tabindex patterns; violations use kb- prefixed rule keys and flow through the same Finding + Issue pipeline; toggleable via KEYBOARD_ENABLED
  • Interactive Element Checks — A live interaction runner (interactiveRunner.js) performs 8 per-page checks by simulating Tab key navigation and pointer interaction: missing focus indicators, unreachable interactive elements, keyboard focus traps, tab order mismatches, hover and focus colour contrast failures, reflow at 320 px, and reduced-motion preference compliance; violations use int- prefixed rule keys and flow through the same Finding + Issue pipeline; toggleable via INTERACTIVE_ENABLED
  • Server-Side Reading Metrics — During a scan each page's visible text is extracted by the crawler and stored on the ScanPage record; ContentMetricsService computes Flesch-Kincaid grade level, reading ease score, word count, and estimated reading time server-side — no LLM calls required
  • Scan Journeys — Define ordered multi-step URL sequences (journeys) per property; each journey is a named, reusable list of labelled pages that can be run as a single targeted scan, enabling consistent auditing of critical user flows such as checkout, login, or onboarding
  • Single-Page Scan — Scope any scan to a single URL by providing a target_url; the crawler is forced into single-page mode, auditing only that URL rather than crawling the whole domain — ideal for quick spot-checks of a specific page without triggering a full site crawl
  • User Avatars — Users can upload a profile picture (JPEG, PNG, GIF, or WebP up to 2 MB) from the Profile settings page; avatars are stored on the public disk and displayed throughout the UI
  • PDF Accessibility Scanning — Automatically discovers linked PDFs during a crawl and audits each document against PDF/UA-1 rules via the veraPDF REST microservice; violations are stored with rule key, severity, WCAG criterion, description, and page number
  • Issue Deduplication & Tracking — Aggregates raw findings into unique, trackable issues with occurrence counts, severity, WCAG category, tags, element HTML, help URLs, and lifecycle status (openin_progressresolved)
  • Issue Activity Log — Full audit trail per issue: status changes, assignments, due date updates, bulk actions, and threaded comments
  • Scan Diff — Side-by-side comparison of two scans showing new, resolved, and persisting findings
  • AI Audit Reports — Executive audit summaries with compliance status, top risks, remediation guidance, and ADA legal precedents, generated by GPT-4o or Claude and augmented with a RAG knowledge base
  • AI Issue Clustering — Groups open issues into thematic clusters to surface systemic patterns and prioritise remediation effort
  • AI Risk Advisory — Prioritised remediation plans ranked by impact, ease of fix, and quick-win potential
  • AI Content Auditing — Analyses page content (alt text, link text, headings, form labels, readability, and video/media accessibility) using deterministic crawler checks enriched by AI; reading metrics (Flesch-Kincaid grade, score, reading time) are computed server-side from stored visible text — the LLM focuses on categorising and advising on findings rather than fetching raw HTML
  • AI Governance Reports — Executive-facing reports with narrative summaries, risk trends, severity breakdowns, remediation progress, legal risk ratings, ADA legal precedents, and compliance status; exportable as JSON, CSV, or PDF
  • RAG-Augmented AI — WCAG standards, ADA lawsuit precedents, and remediation patterns are indexed as vector embeddings (pgvector) and injected into AI prompts at runtime for grounded, legally-aware responses
  • Performance Auditing — Per-page Lighthouse scores (performance, accessibility, SEO, best practices) and Core Web Vitals stored as immutable time-series metrics; each page is audited for both mobile and desktop form factors with separate gauge rows and a form-factor toggle in the UI
  • Experience Score — Composite KPI combining four weighted pillars: Accessibility (40%), Performance (25%), Tech Quality / Best Practices (20%), and Discoverability / SEO (15%); normalised per page scanned, tracked over time with a delta chip, and surfaced on the scan detail and property overview pages; also fed into AI Governance reports as a top-level context signal
  • Risk Scoring & Trending — Weighted risk scores at property, organisation, and agency level with point-in-time snapshots and trend visualisation
  • Project Management Integrations — Push issues to Jira, GitHub Issues, Linear, Asana, Wrike, ClickUp, Monday.com, Azure DevOps, Trello, Notion, or Basecamp; bidirectional status sync via webhooks
  • Notification Email Routing — Route scan, failure, report, and issue notifications by category to any number of non-user email addresses per agency (complements per-user preferences)
  • Notification Webhook Routing — Route the same notification categories to Slack (Block Kit), Microsoft Teams (Adaptive Cards), or Discord (embeds) webhooks; URLs are stored encrypted at rest
  • MCP Server — Model Context Protocol endpoint exposing issues, risk summaries, scan findings, remediation guidance, compliance status, legal risk, and a real-time pending-alerts feed to any MCP-compatible AI tool; includes tools to trigger scans and update issue status
  • Scoped API Keys — Machine-to-machine keys with fine-grained scopes for external integrations, the WordPress plugin, and MCP clients; keys carry optional expiry dates with automated expiry notifications (30-day warning) and daily auto-revocation of expired keys
  • SOC2 Activity Feed — Tamper-evident append-only audit log of all security-relevant events (logins, failed logins, API key lifecycle, issue changes, scan outcomes) covering SOC2 CC6/CC7/CC8 controls; searchable and filterable in Settings → Activity Log with Export CSV for auditors; configurable retention window (default 1 year, pruned monthly)
  • SOC2 Access Reviews — Quarterly access review workflows allowing agency admins to confirm or revoke individual user access; reviewers are notified when a new review cycle opens; completed review history is exportable as CSV
  • SOC2 Evidence Package — Single-page evidence hub at Settings → SOC2 Evidence that bundles three point-in-time exports: user/role assignments, API key inventory, and access review history
  • Failed Login Alerting — Consecutive failed login attempts trigger a SuspiciousLoginNotification to the targeted user and agency admins; all failed attempts are recorded in the activity log
  • WordPress Plugin API — Dedicated REST endpoints (GET /api/wordpress/properties, /issues, /risk-summary, POST /api/wordpress/scans) authenticated via scoped wordpress API keys
  • Multi-Tenant Architecture — Agencies contain organisations which contain properties; all data is strictly tenant-isolated
  • Six-Role RBAC — SuperUser, AgencyAdmin, OrgAdmin, PropAdmin, Editor, and Viewer roles assignable at any scope level
  • Team Management — Invite team members via 7-day email tokens with role pre-assignment and forced password reset on first login
  • Notifications — In-app, email, and webhook notifications for scan completions, scan failures, issue assignments, @mentions, and a weekly digest with per-user opt-out preferences
  • Two-Factor Authentication — Fortify-powered 2FA with recovery codes
  • Scheduled Scans — Configurable once-off or recurring scans (daily / weekly / monthly / quarterly)

Tech Stack

Layer Technology
Language PHP 8.2
Framework Laravel 12
Authentication Laravel Fortify v1
Frontend React 19, TypeScript 5.7, Inertia.js v2
Styling Tailwind CSS v4
UI Components Radix UI, Headless UI
Visualisation D3.js v7, Three.js, React Three Fiber, Chart.js
Type-Safe Routing Laravel Wayfinder v0
Crawler Node.js ≥18, Playwright 1.51, axe-core 4.10, Lighthouse 13
Build Tool Vite 7
Database PostgreSQL with pgvector (vector embeddings)
Queue Laravel Queues (database driver)
Real-Time Laravel Reverb v1 (WebSockets)
Testing Pest v3, PHPUnit v11, Jest 30
Code Quality Laravel Pint, ESLint v9, Prettier v3
Dev Environment Laravel Sail (Docker)
Monitoring Laravel Telescope v5
AI Models OpenAI GPT-4o / Anthropic Claude 3.7 Sonnet
MCP Laravel MCP v0

Architecture

The application follows a multi-tenant domain-driven structure:

Agency
└── Organization
    └── Property
        ├── ScanJourney           (ordered multi-step URL sequence)
        │   └── ScanJourneyStep   (labelled URL at a given position)
        └── Scan
            ├── ScanPage          (per-page crawl record; stores screen_reader_completed,
            │                      content_completed, keyboard_completed, interactive_completed,
            │                      and extracted visible_text)
            ├── ScanMetric        (immutable time-series metrics per page)
            ├── Finding           (raw violation from axe-core, screen reader, or content runner)
            ├── Issue             (deduplicated, tracked violation)
            │   ├── IssueActivity (comment / status / assignment log)
            │   └── IssueLink     (linked external PM ticket)
            ├── LighthouseResult  (performance metrics)
            └── PdfDocument       (discovered PDF link)
                └── PdfViolation  (WCAG finding within the PDF)

Scans are orchestrated by queued jobs. RunScanJob invokes the Node.js crawler to discover pages and PDF documents. The crawler now runs five audit passes per page: axe-core (WCAG rule violations), the virtual screen reader runner (40 SR-specific checks), the content quality runner (14 deterministic content checks), the keyboard navigation runner (7 keyboard checks), and the interactive element runner (8 interaction checks); each page's extracted visible text is also returned for server-side reading metrics. RunScanJob then dispatches a Bus::batch() of RunAxeScanPageJob + two RunLighthouseScanJob instances (mobile and desktop) + RunScreenReaderAuditJob + RunContentAuditJob + RunKeyboardAuditJob + RunInteractiveAuditJob per page. All five Finding tracks share the same ProcessHtmlScan pipeline; SR violations use sr- prefixed rule keys, content violations use content- prefixed keys, keyboard violations use kb- prefixed keys, and interactive violations use int- prefixed keys. For each discovered PDF, a PdfDocument record is created and ScanPdfJob is dispatched to the dedicated pdf queue, which calls the veraPDF REST microservice and stores any PdfViolation records. During a scan, ScanProgressUpdated events are broadcast over the private agency.{agencyId} WebSocket channel (via Laravel Reverb) to push real-time progress to connected clients. When the batch completes, the scan transitions to completed, risk snapshots are recorded at the property, organisation, and agency levels, and an AI audit report is optionally generated.

The platform also maintains a suite of on-demand AI intelligence jobs: GenerateIssueClusteringJob groups related issues into themes, GenerateRiskAdvisoryJob surfaces prioritised action plans, GenerateContentAuditJob checks prose-level accessibility, and GenerateGovernanceReportJob assembles executive governance documents. All AI jobs are scoped to either a property, organisation, or agency and store their results as first-class models.

Issues can be forwarded to any connected project management tool via PushIssueToIntegrationJob. The resulting IssueLink record stores the external ticket ID and URL; a webhook endpoint (POST /api/webhooks/integrations/{integration}) handles bidirectional status sync.

The RAG pipeline indexes WCAG standards, ADA lawsuit precedents, and successful remediation patterns as 1536-dimension vectors (OpenAI text-embedding-3-small) stored in PostgreSQL via pgvector with HNSW indexes. At inference time, RagRetrievalService performs cosine-similarity search and injects the most relevant chunks into AI prompts.

An MCP server at /mcp/property-accessibility exposes property issues, risk summaries, and scan findings to any MCP-compatible AI tool, authenticated via a scoped API key.


Installation

Prerequisites

  • PHP 8.2
  • Composer
  • Node.js ≥ 18
  • PostgreSQL with the pgvector extension
  • A Chromium-compatible browser (for Playwright and Lighthouse)

Quick Setup

# Install all dependencies, copy .env, generate key, migrate, generate Wayfinder routes, and build assets
composer run setup
cd crawler && npm install && cd ..

Or step by step:

composer install
npm install
cd crawler && npm install && cd ..

cp .env.example .env
# Edit .env — at minimum configure DB_* and APP_URL
php artisan key:generate

php artisan migrate
php artisan wayfinder:generate
npm run build

Seed Demo Data

Populate the database with a demo agency, organisations, properties, and ADA lawsuit precedents:

php artisan db:seed --class=DemoSeeder
php artisan db:seed --class=LawsuitDataSeeder

Development

# Start HTTP server, queue worker (default + pdf queues), and Vite concurrently
composer run dev

# Build SSR assets, then start HTTP server, queue worker, Pail log viewer, and SSR server
composer run dev:ssr

WebSockets (Real-Time Scan Progress)

Real-time scan progress requires Laravel Reverb. Start it alongside the dev server:

php artisan reverb:start

Ensure REVERB_APP_ID, REVERB_APP_KEY, and REVERB_APP_SECRET are set in .env and that BROADCAST_CONNECTION=reverb.

PDF Accessibility Scanning (Optional)

PDF scanning depends on the veraPDF REST microservice. Start it with Docker:

docker run -d -p 8080:8080 verapdf/rest:latest

Then set in .env:

PDF_SCANNER_URL=http://localhost:8080
PDF_SCANNER_ENABLED=true

AI Features (Optional)

Set AI_DRIVER=openai (or anthropic) and provide the corresponding API key. All crawler and WCAG auditing features work without AI configured.


Environment Variables

Application

Variable Purpose
APP_URL Application base URL
APP_KEY Laravel application encryption key

Database

Variable Purpose
DB_CONNECTION Must be pgsql (pgvector requires PostgreSQL)
DB_HOST Database host
DB_DATABASE Database name
DB_USERNAME Database username
DB_PASSWORD Database password

AI

Variable Purpose
AI_DRIVER Provider: openai or anthropic
OPENAI_API_KEY OpenAI API key (required when driver is openai)
ANTHROPIC_API_KEY Anthropic API key (required when driver is anthropic)
AI_AUDIT_AUTO_GENERATE Auto-generate AI audit on scan completion (true/false)

Crawler

Variable Purpose
CRAWLER_SCRIPT_PATH Path to crawler/scan.js (default: crawler/scan.js)
CRAWLER_TIMEOUT Max seconds for a full crawl (default: 600)
CRAWLER_MAX_PAGES Maximum pages to crawl per scan (default: 50)
CRAWLER_MAX_DEPTH Maximum link depth to follow (default: 5)
CRAWLER_PAGE_TIMEOUT_MS Page load timeout in milliseconds (default: 30000)
CRAWLER_NAV_TIMEOUT_MS Navigation timeout in milliseconds (default: 60000)
CRAWLER_REQUEST_DELAY_MS Delay between page requests (default: 500)
CRAWLER_LOG_LEVEL Crawler stderr verbosity: silent, error, warn, info (default: error)

Lighthouse

Variable Purpose
LIGHTHOUSE_BINARY Path to Lighthouse CLI binary
LIGHTHOUSE_TIMEOUT Max seconds per Lighthouse run (default: 120)
LIGHTHOUSE_ENABLED Toggle Lighthouse performance audits on/off
LIGHTHOUSE_CHROME_PATH Path to Chromium executable used by Lighthouse (optional)

PDF Scanner

Variable Purpose
PDF_SCANNER_URL Base URL of the veraPDF REST microservice (default: http://pdf-scanner:8080)
PDF_SCANNER_TIMEOUT Max seconds per PDF scan request (default: 120)
PDF_SCANNER_ENABLED Enable PDF accessibility scanning (true / false)

Screen Reader & Content Tracks

Variable Purpose
SCREEN_READER_ENABLED Enable virtual screen reader checks per page (default: true); set false to skip the 40 SR checks and store screen_reader_completed as null
CONTENT_ENABLED Enable deterministic content quality checks per page (default: true); set false to skip the 14 content checks and store content_completed as null
KEYBOARD_ENABLED Enable keyboard navigation checks per page (default: true); set false to skip the 7 keyboard checks and store keyboard_completed as null
INTERACTIVE_ENABLED Enable interactive element checks per page (default: true); set false to skip the 8 interactive checks and store interactive_completed as null

Domain

Roles

Role Scope Capabilities
SuperUser Platform Full access across all agencies
AgencyAdmin Agency Manage all organisations and properties within the agency
OrgAdmin Organisation Manage all properties within the organisation
PropAdmin Property Manage a single property including integrations and API keys
Editor Property Create scans, update issues, add comments
Viewer Property Read-only access

Scan Lifecycle

  1. A Scan record is created with status pending
  2. RunScanJob is dispatched; invokes the Node.js crawler to discover all pages
  3. The crawler runs five audit passes per page: axe-core violations, virtual screen reader checks (sr- prefix), deterministic content quality checks (content- prefix), keyboard navigation checks (kb- prefix), and interactive element checks (int- prefix); each page's visible text is extracted and returned for server-side reading metrics
  4. A Bus::batch() of RunAxeScanPageJob + two RunLighthouseScanJob instances (mobile and desktop) + RunScreenReaderAuditJob + RunContentAuditJob + RunKeyboardAuditJob + RunInteractiveAuditJob is dispatched per discovered page; all five violation tracks share the same ProcessHtmlScan Finding + Issue pipeline
  5. On batch completion the scan transitions to completed; risk snapshots are recorded at property, organisation, and agency levels
  6. ScanCompleted is fired — notifies users, sends routed emails, and dispatches webhook payloads
  7. If AI_AUDIT_AUTO_GENERATE=true, GenerateAiAuditJob is dispatched to produce an executive audit report
  8. Any batch failure transitions the scan to failed and fires ScanFailed, which notifies all agency users and triggers routed email and webhook deliveries

Issue Lifecycle

Raw axe-core Finding records are normalised by IssueNormalizer into deduplicated Issue records enriched with WCAG criteria, descriptive tags, help URLs, and the problematic element's HTML. Issues flow through: openin_progressresolved (or ignored / false_positive). Scan journeys can be attached to a scan to scope the crawl to a predefined set of URLs rather than a full-domain crawl. Issues can be assigned to team members, carry AI-generated remediation guidance with code fixes and legal precedents, and log a full activity trail covering status changes, assignments, due date updates, bulk actions, and comments.

Scan Diff

ScanDiffController compares a scan against its most recent preceding completed scan. It fingerprints every Finding to produce three buckets — new findings (introduced in this scan), resolved findings (present in the prior scan but absent now), and an unchanged count — rendered in a unified diff view.

Scan Journeys

A ScanJourney is a named, reusable sequence of labelled URLs scoped to a property. Each journey contains one or more ScanJourneyStep records (ordered by position) that together define a critical user flow — for example, a checkout funnel or an onboarding sequence.

When a scan is created from a journey, the crawler targets only the URLs in that journey's steps rather than crawling the entire domain. This enables fast, focused audits of the pages that matter most without re-scanning the full site.

Journeys are managed at /journeys and can be attached to both one-off and scheduled scans.

Route Description
GET /journeys List all journeys for the agency
GET /journeys/create Create a new journey
POST /journeys Store a new journey with steps
GET /journeys/{id}/edit Edit a journey and its steps
PATCH /journeys/{id} Update a journey
DELETE /journeys/{id} Delete a journey

Single-Page Scan

Any scan can be scoped to a single URL by setting a target_url on the scan record. When present, RunScanJob forces the crawler into single-page mode — only that URL is crawled, and axe-core and Lighthouse are run against it exclusively. This is useful for quick spot-checks of a specific page without triggering a full-domain crawl.

The scans index (/scans) offers a toggle to enable single-page mode and a URL input field. Scans created in this mode display a Single Page badge alongside the scan entry.

The scan detail page (/scans/{id}) includes a Generate Reports panel that provides one-click dispatch of on-demand AI analysis jobs directly from the scan view:

Button Job dispatched
Generate AI Audit GenerateAiAuditJob
Generate Content Audit GenerateContentAuditJob
Generate Risk Advisory GenerateRiskAdvisoryJob

AI Intelligence Suite

Four AI analysis types sit above the scan layer, each scoped to a property, organisation, or agency:

Type Model Description
Audit Audit Executive summary of a scan's findings with pass/fail compliance scoring
Issue Clusters IssueCluster Groups open issues into thematic clusters to reveal systemic problems; detail page at /issue-clusters/{id}
Risk Advisory RiskAdvisory Prioritised list of remediation recommendations ranked by impact and ease of fix
Content Audit ContentAudit Prose-level analysis of page content for accessibility and readability issues beyond automated rule checks
Governance Report GovernanceReport Comprehensive executive report with narrative, risk trends, severity breakdown, remediation progress, and compliance status

Governance reports support configurable date ranges and can be scheduled alongside scans. They are exportable in JSON, CSV, and PDF formats.

Project Management Integrations

Issues can be pushed to external project management tools via the Integration model and PushIssueToIntegrationJob. An IssueLink record stores the external ticket ID, URL, and status for each linked issue.

Provider Auth Type Bidirectional Webhooks
Jira Basic
GitHub Issues Token
Linear Token
Asana Token
Wrike Token
ClickUp Token
Monday.com Token
Azure DevOps PAT
Trello API Key
Notion Token
Basecamp Token

Integrations are configured per agency in Settings → Integrations. Webhooks from providers are received at POST /api/webhooks/integrations/{integration} to sync status back to IssueLink records.

Webhook Security

Providers that support webhooks can optionally have a webhook_secret credential stored on the integration. When present, the platform verifies the incoming request signature before processing:

  • Jira, Linear, Asana, Wrike, GitHub — HMAC-SHA256 signature verified against the provider-specific header
  • Linear — HMAC-SHA256 on X-Linear-Signature
  • ClickUp — HMAC-SHA256 on X-Signature
  • Basecamp — HMAC-SHA256 on X-Signature-256 (sha256= prefix stripped before comparison)
  • Trello — HMAC-SHA1 base64 on X-Trello-Webhook using the integration's api_secret credential
  • Azure DevOps — Basic auth password comparison via hash_equals
  • Notion — HMAC-SHA256 verified against the X-Notion-Signature header using the optional webhook_secret credential field; requests without a configured secret are accepted (permissive fallback for backward compatibility)

All providers fall back to accepting requests when no secret or credential is configured.

MCP Server

The platform exposes a Model Context Protocol server at /mcp/property-accessibility, authenticated via an API key with the mcp scope.

Type Name Description
Tool GetPropertyIssuesTool Query open accessibility issues for a property
Tool GetIssueRemediationTool Fetch AI-generated remediation guidance for a specific issue
Tool GetScanFindingsTool Read raw axe-core findings from a completed scan
Tool GetRelatedLawsuitsTool Retrieve relevant ADA lawsuit cases and outcomes for a rule
Tool GetSimilarRemediationsTool Find remediation patterns from similar previously resolved issues
Tool TriggerScanTool Initiate a new accessibility scan for a property
Tool UpdateIssueStatusTool Update the status of an issue with optional resolution notes
Resource PropertyIssuesResource Structured issue list keyed by property slug
Resource PropertyRiskSummaryResource Risk score summary for a property
Resource PropertyComplianceResource WCAG compliance breakdown (A/AA/AAA pass/fail counts) for a property
Resource PropertyLegalRiskResource Issues with high legal exposure mapped to ADA lawsuit precedents
Resource PendingAlertsResource Real-time feed of failed scans, critical issues, overdue properties, and running scans
Prompt RemediateViolationPrompt Guided prompt for remediating a specific accessibility violation

API Keys

Scoped API keys allow machine-to-machine access without user credentials. Keys are managed in Settings → API Keys.

Scope Purpose
scans:read View scan results and history
scans:trigger Initiate new scans via API
issues:read Read accessibility issues
reports:read Access governance reports
mcp Connect AI tools via MCP protocol
wordpress Authenticate the WordPress plugin

SOC2 Activity Log

An append-only activity_logs table records all security-relevant events across the platform, supporting SOC2 CC6 (Logical Access), CC7 (System Operations), and CC8 (Change Management) controls.

Logged Events

Category Events
Authentication User login, logout, failed login, password change, 2FA enabled/disabled
Access Control User invited, role changed
API Keys Key created, revoked, used, expiry notified, auto-revoked on expiry
Scans Scan started, completed, failed
Issues Status changed, assigned, comment added
Audit & Reports Audit generated
Properties Property created/updated
Organisations Organisation created/updated
Access Reviews Review started, review completed, user access confirmed, user access revoked
Maintenance Activity log pruned

Implementation

  • Events are written via ActivityLogger — a static service with log() (authenticated user), loginFailed(), loginSuccess(), logoutSuccess(), apiKeyUsed(), logForApiKey() (API key actor), and system() (no-user context) methods
  • Login/logout events fire via Laravel's built-in Auth\Events\Login / Auth\Events\Logout listeners
  • Scan events fire via ScanCompleted / ScanFailed application events
  • Issue changes are captured in IssueObserver; comments in IssueCommentController
  • API key lifecycle is captured in ApiKeyController
  • Each record stores actor_type, actor_label, event, subject_type, subject_id, subject_label, metadata (JSONB), ip_address, and created_at (no updates — append-only)
  • Scoped to the current agency via TenantScope
  • 1-year retention window enforced on the CSV export

Dashboard Activity Tab

The dashboard includes an Activity tab (alongside the Overview tab) showing a paginated, cursor-based feed of the agency's recent events via GET /api/activity-feed.

Activity Log Viewer

Agency admins can browse, search, and filter the full activity log at Settings → Activity Log (/settings/activity-log). The page supports filtering by category (authentication, access control, API keys, scans, issues, etc.) and date range, with colour-coded category badges and cursor-based pagination. The Export CSV button on this page triggers the same CSV stream as the dedicated export endpoint.

SOC2 Export

Agency admins can download the last 365 days of activity logs as a CSV from GET /settings/activity-log/export. The response streams in chunks of 500 records with appropriate Content-Disposition headers.

Activity Log Retention

A configurable retention window (default 12 months, set via app.activity_log_retention_months) is enforced by the activity-log:prune Artisan command, which runs on a monthly schedule. Pruning deletes records older than the threshold on a per-agency basis and writes an ActivityLogPruned entry to record the count of removed rows per agency.


SOC2 Access Reviews

Quarterly access review cycles allow agency admins to verify that every user's role and access level is still appropriate. The access-reviews:create command (scheduled quarterly) generates a new AccessReview record per agency and notifies all AgencyAdmin users via AccessReviewDueNotification. Admins review each user in Settings → Access Reviews, confirming or revoking access one user at a time, before marking the review complete.

Route Description
GET /settings/access-reviews List all access review cycles for the agency
GET /settings/access-reviews/{id} Detail page — confirm or revoke users per review
POST /settings/access-reviews/{id}/users/{user}/confirm Mark a user's access as confirmed
POST /settings/access-reviews/{id}/users/{user}/revoke Revoke a user's access from within the review
POST /settings/access-reviews/{id}/complete Mark the review cycle as completed

SOC2 Evidence Package

Settings → SOC2 Evidence (/settings/soc2-evidence) is a single-page hub that aggregates point-in-time compliance exports for auditors.

Export Route Contents
User Roles GET /settings/soc2-evidence/export/user-roles All users with their roles, scope, and assignment date
API Key Inventory GET /settings/soc2-evidence/export/api-keys All keys with scope, expiry, creator, and status
Access Review History GET /settings/soc2-evidence/export/access-reviews All completed review cycles with per-user outcomes

Failed Login Alerting

Consecutive failed login attempts for an account are recorded as failed_login events in the activity log. When the threshold is crossed, a SuspiciousLoginNotification is dispatched to the affected user and all AgencyAdmin users in the relevant agency, including the source IP address and timestamp.

Per-User Notification Preferences

Users manage preferences per notification type and channel in Settings → Notifications using an opt-out model (enabled by default).

Notification Trigger
ScanCompletedNotification A scan completes on a property the user follows
ScanFailedNotification A scan fails for any reason
IssueAssignedNotification An issue is assigned to the user
IssueMentionedNotification The user is @mentioned in an issue comment
WeeklyDigestNotification Weekly summary of new/resolved issues and scans
SuspiciousLoginNotification Consecutive failed login attempts detected for an account
AccessReviewDueNotification A new quarterly access review cycle has been created for the agency
ApiKeyExpiringSoonNotification An API key is expiring within 30 days (sent to the key's creator and agency admins)
JobFailedNotification A long-running background job (AI generation, scan) fails unexpectedly

Agency-Level Notification Routing

In addition to per-user preferences, agencies can route notifications by category to any number of additional recipients via Settings → Notification Emails and Settings → Notification Webhooks.

Notification categories:

Category Covers
scans Scan completion notifications
scan_failures Scan failure alerts
reports Weekly digest and governance report events
issues Issue assignment and @mention notifications

Email routing (NotificationEmailRoute) — add one or more email addresses per category. Each address receives a clone of the same notification email sent to individual users.

Webhook routing (NotificationWebhookRoute) — add one or more webhook endpoints per category. Webhook URLs are stored encrypted at rest. Supported platforms:

Platform Payload format
Slack Block Kit (blocks array)
Microsoft Teams Adaptive Card (attachments)
Discord Embeds (embeds array)

Delivery is handled by SendWebhookNotificationJob (queued, 3 attempts, 30 s / 60 s backoff).

Real-Time Broadcasting

Laravel Reverb provides a self-hosted WebSocket server for real-time push events. The application broadcasts to private channels authenticated by the user's agency.

Event Channel Payload
ScanProgressUpdated agency.{agencyId} scan_id, pages_scanned, status

Clients subscribe to the private agency.{agencyId} channel (authorised via routes/channels.php — the user's agency_id must match the channel parameter) to receive live scan progress without polling.

Reverb environment variables:

Variable Purpose
REVERB_APP_KEY Reverb application key
REVERB_APP_SECRET Reverb application secret
REVERB_APP_ID Reverb application ID
REVERB_HOST Reverb server host
REVERB_PORT Reverb server port (.env.example uses 8080 for local development)
REVERB_SCHEME https or http (.env.example uses http for local development)

Risk Scoring

Weighted risk scores are calculated and snapshotted at three levels: PropertyRiskSnapshot, OrganizationRiskSnapshot, and AgencyRiskSnapshot. These snapshots power trend charts and governance reports. All three snapshot tiers run on a daily schedule via dedicated Artisan commands (snapshots:property-risk, snapshots:organization-risk, snapshots:agency-risk).

Tenant API

A tenant-scoped API surface is available for machine-to-machine clients that need per-agency data without a user session. Routes are prefixed with the agency slug and authenticated via a scans:read-scoped API key.

Authentication: Authorization: Bearer <key> — the {tenant} segment is the agency's slug, or alternatively pass X-Tenant: <slug> as a request header.

Endpoint Method Description
/api/{tenant}/risk-summary GET Current risk score and trend data for the agency
/api/{tenant}/scan-activity GET Recent scan activity across all properties
/api/{tenant}/issues GET Open issue summary aggregated across all properties
/api/{tenant}/governance-summary GET Latest governance report summary for the agency

WordPress Plugin API

A dedicated API surface under /api/wordpress/ provides everything a WordPress plugin needs to surface accessibility data in the CMS. All endpoints require a wordpress-scoped API key.

Endpoint Method Description
/api/wordpress/properties GET List properties the key has access to
/api/wordpress/properties/{slug}/issues GET Recent open issues for a property
/api/wordpress/properties/{slug}/risk-summary GET Current risk score and snapshot data
/api/wordpress/properties/{slug}/scans POST Trigger a new accessibility scan

Global Search

An authenticated search endpoint allows the UI to perform a unified cross-resource search. Requires a minimum of 2 characters; returns up to 5 results per resource type.

Endpoint Method Description
/api/search?q= GET Search properties, organisations, and issues by name, URL, or rule key

PDF Accessibility Scanning

During each crawl the Node.js crawler collects all PDF links on the same domain. After the page batch completes, a ScanPdfJob is dispatched per PDF to the dedicated pdf queue. The job calls the veraPDF REST microservice which downloads the PDF and validates it against the full PDF/UA-1 rule set.

The job maps veraPDF ruleSummaries and checks to flat PdfViolation records using a 30-clause CLAUSE_META lookup table:

Example clauses WCAG Criterion Severity
Tagged content (6.2) 1.3.1 Critical
Document title (7.1) 2.4.2 Serious
Natural language (11) 3.1.1 Serious
Figure alt text (7.3) 1.1.1 Serious
Bookmarks (6.9) 2.4.5 Moderate

Violations are stored as PdfViolation records linked to the PdfDocument. Results are viewable in the scan detail page (PDFs tab) and at /pdf-documents/{id}. The feature is disabled by default and enabled via PDF_SCANNER_ENABLED=true.

PdfScannerHealthService pings the microservice's /api/info endpoint (3 s timeout) and caches the result for 60 seconds. When the service is unreachable or PDF_SCANNER_ENABLED is false, the PDFs tab on the scan detail page displays an inline availability warning.

Docker: The veraPDF REST service is declared in docker-compose.yml as verapdf/rest:latest, exposing port 8080 (API) and 8081 (Dropwizard diagnostics). JVM heap is capped via JAVA_OPTS=-Xmx384m.


AI Agents

All agents use Laravel AI with structured JSON output and a 300-second timeout.

Agent Input Output
AuditAgent Scan findings, property context, RAG overall_score, executive_summary, compliance_status, top_risks, issue_details, legal_precedents
RemediationAgent Single issue + WCAG context, RAG explanation, wcag_reference, wcag_level, user_impact, severity_rating, code_fix, aria_fix, remediation_steps, testing_guidance, estimated_effort, resources
ContentAuditAgent Per-page findings (from crawler tracks) + server-computed reading metrics content_issues per category (alt_text, link_text, heading_structure, form_label, readability, video_media), suggested_alt_text for image issues; reading metrics are injected as context, not returned by the LLM
RiskAdvisoryAgent Open issues, property/org/agency scope, RAG Prioritised list with severity, risk_reduction_score, ease_of_remediation, quick_win
IssueClusterAgent Open issue set clusters with cluster_name, common_component, recommended_fix, severity (critical/high/medium/low), priority (high/medium/low), issue_ids, wcag_categories, affected_pages, ai_notes
GovernanceAgent Audit + scan + content audit data, RAG executive_narrative, summary_cards, recommendations, source_refs

RAG Knowledge Base

Three embedding stores (pgvector, HNSW index, 1536 dimensions):

Store Contents
WcagEmbedding WCAG 2.1/2.2 success criteria chunks
LawsuitEmbedding ADA lawsuit precedents with case name, year, outcome, and industry
RemediationEmbedding Successful remediation patterns indexed per rule key

RagRetrievalService exposes findWcagChunks(), findLawsuits(), and findSimilarRemediations() for cosine-similarity lookup at inference time.

If the WCAG knowledge base has not been indexed (WcagEmbedding table is empty), the dashboard displays a prominent dismissable warning prompting an admin to run php artisan rag:index-wcag.


Content Audit

The content audit pipeline has two complementary tracks that run for every scan and feed into AI-powered analysis.

Deterministic Content Track (crawler-side)

contentRunner.js runs 14 checks per page using page.evaluate() DOM inspection — no LLM involved. Violations use content- prefixed rule keys and are stored as Finding and Issue records through the same pipeline as axe-core results.

Category Checks
Images Filename-as-alt, generic alt placeholder, alt text > 150 chars, redundant "image of" prefix
Links Raw URL as link text, duplicate link text pointing to different destinations
Headings Multiple <h1> elements on one page
Document Generic page title ("Home", "Untitled", etc.)
Video/Media HTML5 video missing <track kind="captions">, HTML5 video/audio missing transcript link, YouTube embed with unknown caption status, Vimeo embed with unknown caption status, unrecognised video embed service

Each page's visible text (nav/header/footer/scripts stripped, truncated to 8 000 chars) is also extracted by the crawler and stored on the ScanPage record as visible_text.

Server-Side Reading Metrics

ContentMetricsService pulls stored visible_text from ScanPage records and computes per-page readability metrics using the Flesch-Kincaid formulae — no HTTP requests to the page and no LLM required:

Metric Description
word_count Total word count
reading_time Formatted estimate at 230 WPM (e.g. "3 min 20 sec")
reading_time_seconds Raw seconds for programmatic use
flesch_score Flesch Reading Ease (0–100; higher = easier)
reading_level Flesch-Kincaid Grade Level (e.g. "Grade 8 (Flesch-Kincaid)")

AI Content Audit Layer

AiContentAuditService feeds the deterministic findings and server-computed reading metrics into ContentAuditAgent. The LLM receives a list of pre-identified findings per page (not raw HTML) and returns structured advice per content category. The AI output covers:

Field Description
category alt_text, link_text, heading_structure, form_label, readability, or video_media
issue_type Short human-readable label
element_html The offending element
suggestion Actionable recommendation
suggested_alt_text For alt_text issues: exact string for the alt attribute; "" for decorative images; null otherwise
writer_note Guidance for content editors
developer_note Guidance for developers
wcag_criteria Applicable WCAG criterion (e.g. 1.1.1)
severity critical, serious, moderate, or minor

Exports

Reports can be exported via the Exportable concern:

Resource Formats
AI Audit Reports JSON, CSV, PDF
Governance Reports JSON, CSV, PDF
Risk Advisories JSON, CSV, PDF
Content Audits JSON, CSV, PDF
Scans JSON, CSV

Background Jobs

Job Purpose Retries Timeout
RunScanJob Orchestrates full scan lifecycle 3 (10s / 30s backoff) 600s
RunAxeScanPageJob Runs axe-core audit on a single page 2 (10s / 30s backoff) 120s
RunLighthouseScanJob Runs Lighthouse performance audit on a single page (dispatched twice per page: mobile and desktop) 2 (30s backoff) 180s
RunScreenReaderAuditJob Persists pre-computed screen reader violations (from screenReaderRunner.js) through the Finding + Issue pipeline; always marks screen_reader_completed 2 (15s backoff) 60s
RunContentAuditJob Persists pre-computed content violations (from contentRunner.js) and stores visible_text on ScanPage; always marks content_completed 2 (15s backoff) 60s
RunKeyboardAuditJob Persists pre-computed keyboard navigation violations (from keyboardRunner.js) through the Finding + Issue pipeline; always marks keyboard_completed 2 (15s backoff) 60s
RunInteractiveAuditJob Persists pre-computed interactive element violations (from interactiveRunner.js) through the Finding + Issue pipeline; always marks interactive_completed 2 (15s backoff) 60s
GenerateAiAuditJob Creates AI-powered audit report from scan data 2 (60s / 120s backoff) 300s
GenerateIssueRemediationJob Generates AI remediation suggestion for an issue 2 (30s / 60s backoff) 120s
GenerateIssueClusteringJob Clusters open issues into themes via AI 2 (60s / 120s backoff) 300s
GenerateRiskAdvisoryJob Produces prioritised risk recommendations via AI 2 (60s / 120s backoff) 300s
GenerateContentAuditJob Runs AI content accessibility analysis on pre-computed findings and stored visible text 2 (60s / 120s backoff) 300s
GenerateGovernanceReportJob Assembles a full AI-generated governance report 2 (60s / 120s backoff) 300s
PushIssueToIntegrationJob Pushes an issue to an external PM tool 3 (30s / 120s backoff)
ScanPdfJob Downloads a PDF and runs PDF/UA-1 checks via the veraPDF REST microservice; stores PdfViolation records 2 (30s / 60s backoff) 120s
SendWebhookNotificationJob Delivers a notification payload to a Slack, Teams, or Discord webhook URL 3 (30s / 60s backoff)
EmbedWcagDocumentJob Embeds a WCAG document chunk into the vector store 3 (30s / 60s / 120s backoff) 120s
IngestLawsuitDataJob Ingests an ADA lawsuit record into the vector store 3 (30s / 60s / 120s backoff) 120s
IndexRemediationPatternJob Indexes a resolved issue's remediation pattern into the vector store 3 (30s / 60s / 120s backoff) 120s

Artisan Commands

Command Description
php artisan scans:run-scheduled Execute all pending scheduled scans
php artisan scans:expire-stuck Fail any scans stuck in the running state for >20 min
php artisan snapshots:property-risk Record a point-in-time risk snapshot for every property
php artisan snapshots:organization-risk Record a point-in-time risk snapshot for every organisation
php artisan snapshots:agency-risk Record a point-in-time risk snapshot for every agency
php artisan governance:generate-reports Generate scheduled governance reports
php artisan digest:weekly Send weekly accessibility digest emails to all users
php artisan app:backfill-user-roles Populate historical user role records (--dry-run to preview without writing)
php artisan access-reviews:create Create a new quarterly access review cycle for every agency (runs quarterly)
php artisan api-keys:notify-expiring Send expiry warning notifications for API keys expiring within 30 days (runs daily)
php artisan api-keys:revoke-expired Auto-revoke any API keys past their expiry date (runs daily)
php artisan activity-log:prune Delete activity log entries older than the configured retention window (runs monthly)
php artisan rag:index-wcag Dispatch jobs to embed all WCAG criteria chunks into the vector store
php artisan rag:index-lawsuits Dispatch jobs to embed ADA lawsuit records into the vector store
php artisan rag:reindex Re-index one or more RAG stores (wcag, lawsuits, remediations)
php artisan rag:reindex-remediations Index resolved issues with AI suggestions into the remediation vector store
php artisan rag:status Display the current status of all RAG embedding tables

Crawler

The Node.js crawler in crawler/ is a standalone CLI tool invoked by RunScanJob:

node crawler/scan.js <url> \
  [--max-pages 50] \
  [--max-depth 5] \
  [--wcag-version wcag21|wcag22] \
  [--include <pattern>]... \
  [--exclude <pattern>]...

It uses headless Playwright to crawl the target domain, respects robots.txt and domain boundaries, and outputs a JSON object to stdout with two keys: pages (per-page results from all three audit passes) and pdfs (deduplicated list of discovered PDF URLs on the same domain). All diagnostic output is written to stderr only.

Each page entry in pages includes:

  • url — the page URL
  • violations — axe-core WCAG violations
  • srViolations — virtual screen reader violations (sr- prefix; present when SCREEN_READER_ENABLED=true)
  • contentViolations — deterministic content quality violations (content- prefix; present when CONTENT_ENABLED=true)
  • keyboardViolations — keyboard navigation violations (kb- prefix; present when KEYBOARD_ENABLED=true)
  • interactiveViolations — interactive element violations (int- prefix; present when INTERACTIVE_ENABLED=true)
  • visibleText — stripped visible page text (up to 8 000 chars) used for server-side Flesch-Kincaid computation
File Purpose
crawler/scan.js Main entry point; orchestrates all runners and collects per-page results
crawler/axeRunner.js axe-core audit execution
crawler/screenReaderRunner.js Virtual screen reader simulation — 40 checks across 7 categories (sr- prefix)
crawler/contentRunner.js Deterministic content quality checks — 14 checks across 5 categories (content- prefix); also extracts visibleText
crawler/keyboardRunner.js Keyboard navigation checks — 7 checks covering tabindex misuse, onclick-without-keyboard handlers, offscreen-focusable elements, and composite widget patterns (kb- prefix)
crawler/interactiveRunner.js Interactive element checks — 8 checks covering focus indicators, unreachable elements, focus traps, tab order, hover/focus contrast, reflow, and reduced-motion compliance (int- prefix)
crawler/crawlUtils.js URL normalisation, link extraction, PDF link extraction, robots.txt parsing
crawler/config.js Playwright/axe configuration (environment-driven); feature flags for each runner

Screen Reader Runner (screenReaderRunner.js)

40 deterministic WCAG checks grouped into 7 categories, executed via page.evaluate() DOM inspection:

Category Checks
A — Landmarks & Structure Missing main landmark, missing page title, missing h1, skipped heading levels, duplicate unlabelled landmarks, duplicate heading text
B — Interactive & Forms Unlabelled interactive elements, generic link text, ambiguous button text, placeholder-only labels, required field not announced, missing fieldset legend, select with no label, form error not associated with input, error not in live region, status message not announced
C — Images & Media Image with no alt, decorative image with title announced, redundant alt text, SVG with no label, icon button with no label
D — Focus Management Focus order mismatch, missing skip link, non-functional skip link, keyboard trap, focus not visible, modal focus not trapped, modal focus not returned
E — Dynamic Content Silent live region, live region with aria-live="off", expanded state not announced, loading state not announced, tooltip not announced
F — Tables Table with no headers, header cells not associated with data cells, table missing caption, layout table with header markup
G — Language & Other Missing lang attribute on <html>, content before navigation, off-screen content announced by screen readers

Content Runner (contentRunner.js)

14 deterministic checks across 5 categories, plus visible text extraction for server-side reading metrics:

Category Checks
Images Filename-as-alt (content-img-filename-alt), generic alt placeholder (content-img-generic-alt), alt > 150 chars (content-img-long-alt), redundant "image of" prefix (content-img-redundant-descriptor)
Links Raw URL as link text (content-link-url-as-text), duplicate link text to different destinations (content-duplicate-link-text)
Headings Multiple <h1> elements (content-multiple-h1)
Document Generic page title (content-generic-page-title)
Video/Media HTML5 video missing captions track, HTML5 video/audio missing transcript, YouTube embed unknown captions, Vimeo embed unknown captions, unrecognised embed service

Keyboard Runner (keyboardRunner.js)

7 deterministic WCAG checks executed via page.evaluate() DOM inspection:

Check Impact Description
kb-positive-tabindex Serious Element uses tabindex > 0, overriding natural tab order
kb-non-interactive-focusable Moderate Non-interactive element made focusable with no ARIA role or keyboard handler
kb-onclick-no-keyboard Critical onclick handler present but no keyboard event handler — keyboard-only users cannot trigger the action
kb-autofocus-misuse Moderate autofocus applied to a non-form element or to multiple elements
kb-offscreen-focusable Serious Visually hidden element (clip/clip-path) remains in the tab order
kb-aria-disabled-focusable Moderate aria-disabled="true" element still in the tab order because tabindex="-1" is not set
kb-composite-widget-no-roving Serious Composite widget (tablist, listbox, menu, radiogroup) does not implement the roving tabindex pattern

Interactive Runner (interactiveRunner.js)

8 checks across four phases executed via live Playwright interaction (Tab navigation, pointer hover, and viewport resize):

Phase Check Impact Description
Tab Navigation int-focus-indicator-missing Serious Element receives focus but has no visible focus indicator
Tab Navigation int-unreachable-interactive Critical Interactive element not reachable via Tab key
Tab Navigation int-focus-trap Critical Keyboard focus became trapped — no path to escape
Tab Navigation int-focus-order-wrong Moderate Tab order does not match visual reading order
Interaction State int-hover-contrast-fail Serious Insufficient colour contrast on hover
Interaction State int-focus-contrast-fail Serious Insufficient colour contrast when focused
Reflow int-reflow-horizontal-scroll Serious Horizontal scrolling required at 320 px viewport width
Reduced Motion int-reduced-motion-ignored Moderate Animated content does not respect prefers-reduced-motion: reduce

PDF Scanner Microservice

PDF accessibility validation is handled by veraPDF REST (verapdf/rest:latest), a standards-compliant PDF/UA-1 validator. It exposes:

  • POST /api/validate/url/ua1 — accepts {url}, validates the PDF against PDF/UA-1, returns a JSON report with ruleSummaries and per-rule checks
  • GET /api/info — readiness probe (Dropwizard diagnostics)

The service is declared in docker-compose.yml (port 8080). Run it with:

docker compose up pdf-scanner

Scripts

PHP / Composer

composer run setup      # Full initial setup (install, migrate, generate Wayfinder routes, build)
composer run dev        # Start server, queue worker (default + pdf), and Vite concurrently
composer run dev:ssr    # Build SSR assets, then start server, queue worker, Pail, and SSR server
composer run lint       # Run Laravel Pint formatter
composer run test:lint  # Pint lint check only (no tests)
composer run test       # Lint check + full test suite

Node / npm

npm run dev             # Start Vite dev server
npm run build           # Production asset build
npm run build:ssr       # SSR + client build
npm run lint            # ESLint with auto-fix
npm run format          # Prettier format
npm run types           # TypeScript type check

Testing

# Run all tests
php artisan test --compact

# Run a specific file
php artisan test --compact tests/Feature/Jobs/GenerateContentAuditJobTest.php

# Run a specific test by name
php artisan test --compact --filter="it persists legal_precedents"

# Full suite with lint check
composer run test

Crawler unit tests (Jest):

cd crawler && npm test

Tests use Pest v3 with Ai::fakeAgent() for structured AI output faking, Http::fake() for HTTP mocking, and RefreshDatabase for database isolation.


Key Configuration Files

File Purpose
config/ai.php AI driver selection, model, temperature, token limits
config/crawler.php Crawler timeout, max depth, max pages
config/lighthouse.php Lighthouse binary path, timeout, feature flag, Chrome path
config/screen_reader.php Screen reader runner feature flag (SCREEN_READER_ENABLED)
config/content.php Content runner feature flag (CONTENT_ENABLED)
config/keyboard.php Keyboard runner feature flag (KEYBOARD_ENABLED)
config/interactive.php Interactive element runner feature flag (INTERACTIVE_ENABLED)
config/fortify.php Authentication feature flags (2FA, email verification, etc.)
config/services.php External service configuration including pdf_scanner (url, timeout, enabled)

Settings

Page Route Description
Profile /settings/profile Name, email, and account details; upload or remove a profile avatar
Password /settings/password Change account password
Two-Factor Auth /settings/two-factor Enable/disable 2FA and manage recovery codes
Appearance /settings/appearance Theme and UI preferences
Notifications /settings/notifications Per-channel notification opt-out preferences
Scheduled Scans /settings/scheduled-scans Manage recurring scans
API Keys /settings/api-keys Create and revoke scoped API keys; expiry status badges and alert banner for keys expiring within 30 days
Integrations /settings/integrations Connect and manage project management integrations
Activity Log /settings/activity-log Browse and filter the full activity log with category and date range filters
Activity Log Export /settings/activity-log/export Download SOC2 audit log as CSV (last 365 days)
Access Reviews /settings/access-reviews Manage quarterly SOC2 access review cycles
Notification Emails /settings/notification-email-routes Route notification categories to additional non-user email addresses
Notification Webhooks /settings/notification-webhook-routes Route notification categories to Slack, Teams, or Discord webhook URLs
SOC2 Evidence /settings/soc2-evidence Download user-role, API key, and access review exports for auditors

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for setup instructions, branch naming conventions, and the PR checklist.

To report a security vulnerability, see SECURITY.md — please do not use public issues for security reports.

License

A11y Engine is open-source software licensed under the GNU Affero General Public License v3.0.

Under AGPL v3, you are free to use, modify, and self-host this software. If you distribute a modified version or offer it as a network service, you must make your modified source code available under the same licence.

About

A web accessibility auditing and risk management platform.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages