Skip to content

rush-cms/audits

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rush CMS Audits Banner

Rush CMS Audits Microservice

Laravel 12 PHPStan Level 8 Tests Passing License MIT

rushcms.com/audits


A standalone, headless microservice dedicated to generating high-fidelity performance reports (Lighthouse/PageSpeed) in PDF format. Designed to be whitelabel, asynchronous, and webhook-oriented.

Ideally used with n8n or other automation tools to ingest PageSpeed API data and return professional client-ready PDFs.

Documentation: docs/

Features

  • Headless Architecture: API-first design with no frontend UI
  • Whitelabel Ready: Customize logos, brand names, CTA links, and more via config
  • Asynchronous Processing: Heavy PDF generation happens in the background via Queues
  • Webhook Callbacks: Receive a ping with the PDF URL as soon as it's ready
  • Webhook Signatures: HMAC-SHA256 signatures for webhook authenticity verification
  • Audit Persistence: All audits stored in database with status tracking (pending, processing, completed, failed)
  • State-Based Idempotency: Smart idempotency based on audit status, not time windows
  • Graceful Degradation: PDF generation continues even if screenshots fail (configurable)
  • Partial Data Persistence: PageSpeed data preserved even if pipeline fails midway
  • Dead Letter Queue: Failed jobs tracked with auto-cleanup after configurable retention period
  • Smart Pruning: Auto-cleanup of old PDF files to save storage
  • Token Authentication: Built-in console commands to manage API clients
  • Rate Limiting: Configurable per-token and global rate limits with Redis
  • SSRF Protection: Blocks private networks and localhost in production
  • Race Condition Handling: Optimistic locking with exponential backoff for concurrent requests
  • Multi-language Support: English, Portuguese (BR), and Spanish
  • Screenshot Capture: Automatic desktop & mobile screenshots with device mockup frames
  • SEO & Accessibility Audits: Optional sections with detailed issue reports
  • Core Web Vitals: LCP, FCP, CLS with contextual performance messages

Tech Stack

  • Core: Laravel 12 API (Slim setup)
  • PDF Engine: Spatie Browsershot (Puppeteer/Chromium)
  • Image Processing: Spatie Image (WebP conversion)
  • Queue: Redis (Recommended)
  • Storage: Local Public Disk / S3

Workflow

  1. Ingestion: Send a URL to POST /api/v1/scan
  2. State-Based Idempotency: Returns existing audit based on status (pending/processing = reuse, completed = new scan allowed, failed recent = retry window)
  3. Database Persistence: Creates audit record with status pending
  4. PageSpeed Fetch: Fetches Performance, SEO & Accessibility data, saves to pagespeed_data, marks audit as processing
  5. Screenshot Capture: Takes desktop (1920x1080) and mobile (375x812) screenshots, saves to screenshots_data
  6. Image Optimization: Converts screenshots to WebP (600px width)
  7. PDF Generation: Renders Blade views with Tailwind CSS, converts to PDF (continues even if screenshots fail)
  8. Status Update: Marks audit as completed with score, metrics, and PDF path
  9. Callback: Sends POST request to your webhook URL with the PDF link and metadata

Roadmap

Some features are not yet implemented, but are planned for the future:

  • S3/R2 storage support
  • CORS configuration
  • Factories for audit testing
  • Admin dashboard with template customization
  • Audit comparisions
  • Internal check-ups with advanced SEO metrics

Environment Configuration

cp .env.example .env

Edit the .env file to configure:

APP_URL=https://audits.rushcms.com
APP_TIMEZONE="America/Sao_Paulo"

# Service Logic
AUDITS_WEBHOOK_RETURN_URL="https://your-main-app.com/api/webhook/audit-ready"
AUDITS_CONCURRENCY=1
AUDITS_RETENTION_DAYS=7
AUDITS_IDEMPOTENCY_WINDOW=60
AUDITS_FAILED_RETRY_AFTER=300
AUDITS_REQUIRE_SCREENSHOTS=false
AUDITS_FAILED_JOBS_RETENTION_DAYS=30
AUDITS_JOB_MAX_ATTEMPTS=3
AUDITS_JOB_BACKOFF_BASE=30

# Branding (Whitelabel)
AUDITS_BRAND_NAME="Rush CMS"
AUDITS_LOGO_PATH="rush-cms-logo.png"

# Report Settings
AUDITS_DATE_FORMAT="d/m/Y H:i"
AUDITS_SHOW_SEO=false
AUDITS_SHOW_ACCESSIBILITY=false
AUDITS_CTA_URL="https://wa.me/5511999999999"

# PageSpeed Insights API (optional, increases rate limits)
PAGESPEED_API_KEY=

# Browsershot / Puppeteer (Critical for Linux/Docker)
BROWSERSHOT_NODE_BINARY="/usr/bin/node"
BROWSERSHOT_NPM_BINARY="/usr/bin/npm"
BROWSERSHOT_CHROME_PATH="/usr/bin/google-chrome"

Configuration Reference

Variable Default Description
APP_TIMEZONE America/Sao_Paulo Timezone for date display
AUDITS_BRAND_NAME Rush CMS Brand name in header/footer
AUDITS_LOGO_PATH null Path to logo in public folder
AUDITS_DATE_FORMAT d/m/Y H:i PHP date format for header
AUDITS_SHOW_SEO false Show SEO audit section
AUDITS_SHOW_ACCESSIBILITY false Show Accessibility section
AUDITS_CTA_URL WhatsApp link Call-to-action button URL
AUDITS_RETENTION_DAYS 7 Days to keep PDF files before pruning
AUDITS_IDEMPOTENCY_WINDOW 60 Minutes to prevent duplicate audits (legacy)
AUDITS_FAILED_RETRY_AFTER 300 Seconds before failed audit allows retry (5 min)
AUDITS_REQUIRE_SCREENSHOTS false Fail audit if screenshots fail (true) or continue (false)
AUDITS_FAILED_JOBS_RETENTION_DAYS 30 Days to keep failed jobs before cleanup
AUDITS_JOB_MAX_ATTEMPTS 3 Maximum retry attempts for failed jobs
AUDITS_JOB_BACKOFF_BASE 30 Base delay in seconds for job retry backoff
AUDITS_WEBHOOK_TIMEOUT 5 Webhook timeout in seconds
AUDITS_WEBHOOK_CONNECT_TIMEOUT 2 Webhook connection timeout in seconds
AUDITS_WEBHOOK_MAX_ATTEMPTS 5 Maximum webhook retry attempts
AUDITS_WEBHOOK_SECRET null Secret for webhook HMAC signatures
AUDITS_NOTIFY_ON_WEBHOOK_FAILURE true Send notifications on webhook failure
AUDITS_ADMIN_EMAIL null Admin email for failure notifications
AUDITS_SLACK_WEBHOOK_URL null Slack webhook URL for failure notifications
AUDITS_RATE_LIMIT_PER_MINUTE 60 Requests per minute per token
AUDITS_RATE_LIMIT_PER_HOUR 500 Requests per hour per token
AUDITS_RATE_LIMIT_PER_DAY 2000 Requests per day per token
AUDITS_RATE_LIMIT_GLOBAL_PER_MINUTE 200 Global requests per minute
AUDITS_DELETE_SCREENSHOTS_AFTER_PDF true Delete screenshots after PDF generation
AUDITS_ORPHANED_SCREENSHOTS_RETENTION_HOURS 24 Hours before orphaned screenshots are deleted
PAGESPEED_API_KEY null Google PageSpeed API key
PAGESPEED_RATE_LIMIT_PER_MINUTE 6 PageSpeed API calls per minute (free tier)
PAGESPEED_RATE_LIMIT_PER_DAY 25000 PageSpeed API calls per day (with API key)
QUEUE_PDF_CONCURRENCY 3 Max concurrent PDF generation jobs
QUEUE_SCREENSHOT_CONCURRENCY 5 Max concurrent screenshot capture jobs
QUEUE_MAX_DEPTH_ALERT 100 Queue depth threshold for alerts
API_MAX_REQUEST_SIZE 1048576 Max request payload size in bytes (1MB)
BROWSERSHOT_TIMEOUT 60 Browsershot process timeout in seconds
BROWSERSHOT_MEMORY_LIMIT 512 Browsershot max memory in MB per process
BROWSERSHOT_MAX_CONCURRENT_PDF 3 Max concurrent Browsershot PDF processes
BROWSERSHOT_MAX_CONCURRENT_SCREENSHOTS 5 Max concurrent Browsershot screenshot processes

API Reference

Authentication

All requests require a Bearer Token:

Authorization: Bearer <your-token>

Rate Limiting

API requests are rate-limited per token:

  • 60 requests/minute (default)
  • 500 requests/hour (default)
  • 2000 requests/day (default)
  • 200 requests/minute globally (all tokens combined)

Rate limit headers are included in all responses:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1704067200

When limit is exceeded, you'll receive a 429 Too Many Requests response:

{
  "message": "Too many requests. Please try again later.",
  "retry_after": 32
}

Webhook Signatures

If AUDITS_WEBHOOK_SECRET is configured, webhooks include HMAC-SHA256 signatures for verification:

Headers:

X-Webhook-Signature: sha256=abc123...
X-Webhook-Timestamp: 1704067200
X-Webhook-ID: 20250129120000-a1b2c3d4e5f6g7h8

Verification (PHP):

$secret = env('AUDITS_WEBHOOK_SECRET');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$signature = str_replace('sha256=', '', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE']);
$payload = file_get_contents('php://input');

$expectedSignature = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);

if (!hash_equals($expectedSignature, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

if (abs(time() - $timestamp) > 300) {
    http_response_code(401);
    exit('Timestamp expired');
}

Verification (Node.js):

const crypto = require('crypto');

const secret = process.env.AUDITS_WEBHOOK_SECRET;
const timestamp = req.headers['x-webhook-timestamp'];
const signature = req.headers['x-webhook-signature'].replace('sha256=', '');
const payload = JSON.stringify(req.body);

const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(`${timestamp}.${payload}`)
  .digest('hex');

if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
  return res.status(401).json({ error: 'Invalid signature' });
}

if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
  return res.status(401).json({ error: 'Timestamp expired' });
}

Security & SSRF Protection

In production (APP_ENV=production), the service automatically blocks:

  • Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Localhost (127.0.0.1, ::1, localhost)
  • Link-local addresses (169.254.0.0/16 - AWS metadata, etc.)
  • Custom blocked domains (via config/blocked-domains.php)

Example blocked URLs in production:

http://localhost:8000/admin ❌ Blocked
http://192.168.1.1/ ❌ Blocked
http://169.254.169.254/latest/meta-data/ ❌ Blocked (AWS metadata)
https://example.com ✅ Allowed

In local/development (APP_ENV=local), SSRF protection is disabled to allow testing against local services.

Blocking Custom Domains:

Edit config/blocked-domains.php to add domains:

return [
    'internal.company.com',
    'staging.myapp.com',
    'localhost.example.com',
];

Subdomains are automatically matched (e.g., example.com blocks www.example.com).

Submit Scan

Endpoint: POST /api/v1/scan

Request:

{
  "url": "https://example.com",
  "lang": "pt_BR",
  "strategy": "mobile"
}
Field Type Default Description
url string required URL to analyze
lang string en Language: en, pt_BR, es
strategy string mobile Strategy: mobile or desktop

Response: 202 Accepted

{
  "message": "Audit queued",
  "audit_id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://example.com",
  "lang": "pt_BR",
  "strategy": "mobile",
  "status": "pending"
}

Note: Idempotency is now state-based:

  • Pending/Processing audits: Returns existing audit_id
  • Completed audits: Creates new audit (allows immediate re-scan)
  • Failed audits: Returns existing if recent (< 5min), creates new if old

Get Audit Status

Endpoint: GET /api/v1/audits/{id}

Response: 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://example.com",
  "strategy": "mobile",
  "lang": "pt_BR",
  "status": "completed",
  "score": 95,
  "metrics": {
    "lcp": "1.2 s",
    "fcp": "0.8 s",
    "cls": "0.05"
  },
  "pdf_url": "https://audits.rushcms.com/storage/reports/550e8400.pdf",
  "error_message": null,
  "created_at": "2025-12-29T04:00:00Z",
  "completed_at": "2025-12-29T04:02:15Z"
}

Status values: pending, processing, completed, failed

Get Stats

Endpoint: GET /api/v1/stats

Response: 200 OK

{
  "minute": 3,
  "hour": 45,
  "day": 127,
  "month": 1542
}

Full API Documentation: docs/api.md

Console Commands

# Create API token
php artisan audit:create-token "Client Name"

# Test PDF generation (no API calls)
php artisan test:pdf --lang=pt_BR

# Prune old PDFs
php artisan audit:prune-pdfs

# Prune orphaned screenshots
php artisan audits:prune-orphaned-screenshots

# Cleanup old failed jobs
php artisan audits:cleanup-failed-jobs

# Retry failed webhook delivery
php artisan webhook:retry {audit_id}
php artisan webhook:retry-failed --limit=50

# Prune old webhook delivery records
php artisan webhook:prune-deliveries --days=30

# Analyze query performance
php artisan audits:explain-queries

# Check browser setup
php artisan audit:check-browser

Deployment

System Requirements

  • PHP 8.4+
  • Node.js 18+
  • Chromium/Chrome
  • Redis (for queues)

Queue Worker (Required)

php artisan queue:work --tries=2 --timeout=180

Scheduler

php artisan schedule:run

Report Components

The PDF report includes:

  1. Header: Brand logo + generation timestamp
  2. Performance Score: Circular gauge with pass/fail indicator
  3. Device Mockup: iPhone 16 + MacBook frames with live screenshots
  4. Core Web Vitals: LCP, FCP, CLS cards with progress bars
  5. Performance Messages: Contextual feedback based on metrics
  6. SEO Section: Score + failed audit list (optional)
  7. Accessibility Section: Score + failed audit list (optional)
  8. Closing CTA: Dynamic message based on score + WhatsApp button
  9. Footer: Brand name, Audit ID, data source

License

This project is open-sourced software licensed under the MIT License.

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages