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 notificationsKey Contributions
- Designed and built the full stack independently — from Strapi schema design and PostgreSQL modelling to the React frontend and Stripe subscription integration
- Crawling engine using Puppeteer + axe-core that maps WCAG 2.2 success criteria to Lighthouse audit IDs, producing structured per-criterion pass/fail data for every scanned page
- p-queue-based job scheduler with configurable concurrency limits, preventing Puppeteer from overloading the server during large-domain crawls
- Strapi policy middleware enforcing subscription tier limits (domain count and monthly page scans) before any crawl is initiated — structured PolicyError responses flow through to the UI
- PDF audit report generation using @react-pdf/renderer, delivering branded per-domain accessibility reports downloadable from the dashboard
- Stripe Checkout and Customer Portal integration with webhook handling for subscription created, updated, and cancelled lifecycle events
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.
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
- React
- Vite
- TypeScript
- TanStack Router
- TanStack Query
- Radix UI / shadcn
- Tailwind CSS
- Framer Motion
- GSAP
- Strapi
- PostgreSQL
- Node.js
- Puppeteer
- axe-core
- Lighthouse API
- p-queue
- Stripe
- Novu
- Nodemailer
- @react-pdf/renderer
- React Hook Form
- Zod