← Back to projects

Freelance SaaS

Incluscan

2024 – present

Overview

Full-stack SaaS product built independently — an AI-powered WCAG 2.2 accessibility scanner for web teams. Users connect their domains, trigger automated crawls, and receive structured audit reports with per-criterion pass/fail breakdowns, Lighthouse scores, and branded PDF exports. Stripe subscriptions gate domain and page scan limits per tier.

Architecture

Yarn Workspaces Monorepo + Docker Compose
  │
  ├── Frontend (React + Vite)
  │     ├── TanStack Router — type-safe file-based routing
  │     ├── TanStack Query — server state · cache · optimistic UI
  │     ├── Radix UI / shadcn — accessible component primitives
  │     ├── Framer Motion + GSAP — page & UI animations
  │     ├── Stripe.js — subscription checkout & billing portal
  │     └── Novu — in-app notification feed
  │
  └── Backend (Strapi + PostgreSQL)
        ├── Puppeteer + axe-core — WCAG 2.2 page crawling
        ├── Lighthouse API — performance + accessibility scores
        ├── p-queue — rate-limited concurrent scan job queue
        ├── @react-pdf/renderer — branded PDF report generation
        ├── Stripe webhooks — subscription lifecycle management
        └── Novu + Nodemailer — email + push notifications

Key Contributions

Code Examples

Free Scan — Queue, Crawl, Gate

The public free scan runs axe-core via Puppeteer — no auth required, but rate-limited by a p-queue with concurrency: 2. Each Puppeteer instance is memory-intensive, so without the queue a traffic spike would exhaust server RAM. The scan returns two shapes: a partial result (score + top 3 issues ordered by severity) shown immediately, and a full result held server-side until the visitor submits their email — the freemium gate that converts visitors into leads.

backend/src/api/mini-scan/services/mini-scan.ts
import PQueue from "p-queue";

// Each Puppeteer instance is memory-intensive — cap concurrent scans
const scanQueue = new PQueue({ concurrency: 2 });

async function startScan(url: string): Promise<MiniScanResult> {
  return scanQueue.add(async () => {
    const scanId = uuidv4();

    // Run axe-core via Puppeteer (30 s timeout)
    const report = await axeService.analyzeAndTransform({ url, timeout: 30_000 });

    // Flatten violations ordered by severity: critical → serious → moderate → minor
    const allIssues = [
      ...report.issuesBySeverity.critical,
      ...report.issuesBySeverity.serious,
      ...report.issuesBySeverity.moderate,
      ...report.issuesBySeverity.minor,
    ];

    return {
      scanId,
      url,
      accessibilityScore: report.score,
      totalIssues: allIssues.length,
      // Top 3 surfaced immediately — full list gated behind email capture
      topIssues: allIssues.slice(0, 3).map((issue) => ({
        id: issue.id,
        title: issue.title,
        impact: issue.impact,
        affectedElements: issue.occurrences,
      })),
      fullResult: { source: "axe-core", axe: { score: report.score } },
    };
  }) as Promise<MiniScanResult>;
}

// Partial shape returned to the browser before email is submitted
function getPartialResult(result: MiniScanResult) {
  const { fullResult: _, ...partial } = result;
  return partial;
}

// Called after visitor submits email — stores lead + unlocks full report
async function storeLead(email: string, result: MiniScanResult, ip: string) {
  return strapi.documents("api::lead.lead").create({
    data: {
      email,
      scannedUrl: result.url,
      accessibilityScore: result.accessibilityScore,
      topIssues: result.topIssues,
      fullResult: result.fullResult,
      ipAddress: ip,
      consentTimestamp: new Date(),
    },
  });
}

Tech Stack

Frontend
  • React
  • Vite
  • TypeScript
  • TanStack Router
  • TanStack Query
UI & Animation
  • Radix UI / shadcn
  • Tailwind CSS
  • Framer Motion
  • GSAP
Backend
  • Strapi
  • PostgreSQL
  • Node.js
Crawling & Audit
  • Puppeteer
  • axe-core
  • Lighthouse API
  • p-queue
Payments & Notifications
  • Stripe
  • Novu
  • Nodemailer
Reports & Forms
  • @react-pdf/renderer
  • React Hook Form
  • Zod