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.
- 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 usingsr-prefixed rule keys; toggleable viaSCREEN_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 usecontent-prefixed rule keys and flow through the same Finding + Issue pipeline; toggleable viaCONTENT_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-disabledinconsistencies, and composite widget roving-tabindex patterns; violations usekb-prefixed rule keys and flow through the same Finding + Issue pipeline; toggleable viaKEYBOARD_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 useint-prefixed rule keys and flow through the same Finding + Issue pipeline; toggleable viaINTERACTIVE_ENABLED - Server-Side Reading Metrics — During a scan each page's visible text is extracted by the crawler and stored on the
ScanPagerecord;ContentMetricsServicecomputes 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 (
open→in_progress→resolved) - 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
SuspiciousLoginNotificationto 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 scopedwordpressAPI 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)
| 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 |
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.
- PHP 8.2
- Composer
- Node.js ≥ 18
- PostgreSQL with the pgvector extension
- A Chromium-compatible browser (for Playwright and Lighthouse)
# 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 buildPopulate the database with a demo agency, organisations, properties, and ADA lawsuit precedents:
php artisan db:seed --class=DemoSeeder
php artisan db:seed --class=LawsuitDataSeeder# 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:ssrReal-time scan progress requires Laravel Reverb. Start it alongside the dev server:
php artisan reverb:startEnsure REVERB_APP_ID, REVERB_APP_KEY, and REVERB_APP_SECRET are set in .env and that BROADCAST_CONNECTION=reverb.
PDF scanning depends on the veraPDF REST microservice. Start it with Docker:
docker run -d -p 8080:8080 verapdf/rest:latestThen set in .env:
PDF_SCANNER_URL=http://localhost:8080
PDF_SCANNER_ENABLED=true
Set AI_DRIVER=openai (or anthropic) and provide the corresponding API key. All crawler and WCAG auditing features work without AI configured.
| Variable | Purpose |
|---|---|
APP_URL |
Application base URL |
APP_KEY |
Laravel application encryption key |
| 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 |
| 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) |
| 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) |
| 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) |
| 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) |
| 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 |
| 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 |
- A
Scanrecord is created with statuspending RunScanJobis dispatched; invokes the Node.js crawler to discover all pages- 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 - A
Bus::batch()ofRunAxeScanPageJob+ twoRunLighthouseScanJobinstances (mobile and desktop) +RunScreenReaderAuditJob+RunContentAuditJob+RunKeyboardAuditJob+RunInteractiveAuditJobis dispatched per discovered page; all five violation tracks share the sameProcessHtmlScanFinding + Issue pipeline - On batch completion the scan transitions to
completed; risk snapshots are recorded at property, organisation, and agency levels ScanCompletedis fired — notifies users, sends routed emails, and dispatches webhook payloads- If
AI_AUDIT_AUTO_GENERATE=true,GenerateAiAuditJobis dispatched to produce an executive audit report - Any batch failure transitions the scan to
failedand firesScanFailed, which notifies all agency users and triggers routed email and webhook deliveries
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: open → in_progress → resolved (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.
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.
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 |
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 |
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.
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.
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-Webhookusing the integration'sapi_secretcredential - Azure DevOps — Basic auth password comparison via
hash_equals - Notion — HMAC-SHA256 verified against the
X-Notion-Signatureheader using the optionalwebhook_secretcredential 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.
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 |
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 |
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.
| 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 |
- Events are written via
ActivityLogger— a static service withlog()(authenticated user),loginFailed(),loginSuccess(),logoutSuccess(),apiKeyUsed(),logForApiKey()(API key actor), andsystem()(no-user context) methods - Login/logout events fire via Laravel's built-in
Auth\Events\Login/Auth\Events\Logoutlisteners - Scan events fire via
ScanCompleted/ScanFailedapplication events - Issue changes are captured in
IssueObserver; comments inIssueCommentController - 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, andcreated_at(no updates — append-only) - Scoped to the current agency via
TenantScope - 1-year retention window enforced on the CSV export
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.
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.
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.
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.
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 |
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 |
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.
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 |
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).
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) |
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).
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 |
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 |
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 |
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.
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 |
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.
The content audit pipeline has two complementary tracks that run for every scan and feed into AI-powered analysis.
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.
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)") |
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 |
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 |
| 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 |
| 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 |
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 URLviolations— axe-core WCAG violationssrViolations— virtual screen reader violations (sr-prefix; present whenSCREEN_READER_ENABLED=true)contentViolations— deterministic content quality violations (content-prefix; present whenCONTENT_ENABLED=true)keyboardViolations— keyboard navigation violations (kb-prefix; present whenKEYBOARD_ENABLED=true)interactiveViolations— interactive element violations (int-prefix; present whenINTERACTIVE_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 |
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 |
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 |
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 |
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 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 withruleSummariesand per-rulechecksGET /api/info— readiness probe (Dropwizard diagnostics)
The service is declared in docker-compose.yml (port 8080). Run it with:
docker compose up pdf-scannerPHP / 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 suiteNode / 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# 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 testCrawler unit tests (Jest):
cd crawler && npm testTests use Pest v3 with Ai::fakeAgent() for structured AI output faking, Http::fake() for HTTP mocking, and RefreshDatabase for database isolation.
| 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) |
| 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 |
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.
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.