Back to Blog
Tutorial2026-07-018 min read

Build a Privacy-First Self-Hosted AI Accessibility Compliance Checker for Indian Public-Facing Web Apps (GIGW 3.0 & WCAG 2.2) with Next.js, Ollama, and pgvector: 2026 Step-by-Step Guide for Developers

Learn to build a fully self-hosted, privacy-first AI accessibility compliance checker aligned with India's GIGW 3.0 and global WCAG 2.2 standards, using Next.js, Ollama, and pgvector—no third-party data sharing, no recurring SaaS costs, and full control for public sector teams in 2026.

Agentic Academy Labs

Author

Introduction

In 2026, India’s GIGW 3.0 (Government of India Guidelines for Web and Mobile Applications) and global WCAG 2.2 accessibility standards are mandatory for all public-facing web apps handling citizen data—from Aadhaar enrollment portals to state health dashboards. Most off-the-shelf accessibility checkers are SaaS tools that upload sensitive user page data to third-party servers, violating the country’s DPDP Act 2023 and creating unnecessary risk for public sector teams.

This step-by-step guide walks you through building a fully self-hosted, privacy-first AI accessibility compliance checker that runs entirely on your own infrastructure, with zero data exfiltration. We’ll use Next.js 14, Ollama for local LLM inference, and pgvector for fast semantic rule matching, with out-of-the-box support for GIGW 3.0 and WCAG 2.2 requirements.

Why Privacy-First Accessibility Tools Are Non-Negotiable for Indian Public Apps

Public-facing government apps process highly sensitive citizen data: tax records, health information, identity verification details, and more. Sending this data to third-party SaaS accessibility tools creates compliance risk under the DPDP Act, which mandates that personal data stay within controlled, auditable infrastructure.

Beyond compliance, self-hosted tools offer unique benefits for public sector teams:

  • No recurring SaaS costs: Pay only for your own server infrastructure, no per-scan fees
  • Custom rule support: Add GIGW 3.0-specific requirements (like regional language accessibility rules) that generic global checkers miss
  • Full audit control: All scan logs and results stay on your servers for official compliance documentation
  • Air-gap compatibility: Run the entire stack in isolated, offline environments for highly sensitive use cases

Tech Stack Overview

We’ve chosen tools that are widely adopted in Indian government and enterprise stacks, with built-in privacy guarantees:

  • Next.js 14 (App Router): For a fast, accessible frontend with React Server Components to reduce client-side data exposure
  • Ollama: Runs open-source LLMs (we’ll use a Llama 3 8B model fine-tuned on accessibility rules) locally on your server, with no external API calls
  • pgvector: A PostgreSQL extension for vector storage, used to index GIGW 3.0 and WCAG 2.2 rules for fast semantic matching
  • Drizzle ORM: Type-safe database access with built-in pgvector support
  • Tailwind CSS: For building an accessible, WCAG 2.2 AA-compliant UI out of the box

Step 1: Set Up Your Project Foundation

First, ensure you have the following prerequisites installed:

  • Node.js 20+
  • PostgreSQL 16+ with the pgvector extension enabled
  • Ollama (for local LLM and embedding inference)

Create your Next.js project and install dependencies:

npx create-next-app@latest accessibility-checker --typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd accessibility-checker
npm install pg @ollama/ai pgvector drizzle-orm drizzle-kit

Enable pgvector in your PostgreSQL instance, then create a .env file with your database connection string:

DATABASE_URL=postgres://user:password@localhost:5432/accessibility_checker
OLLAMA_HOST=http://localhost:11434

Step 2: Configure Ollama for Local Accessibility Analysis

First, pull a fine-tuned Llama 3 8B model trained on GIGW 3.0 and WCAG 2.2 rule sets, plus a local embedding model for semantic search:

ollama pull llama3:8b-instruct-accessibility
ollama pull nomic-embed-text

Next, build a Next.js API route to call Ollama locally, with no external API keys required. This route will receive page content and relevant accessibility rules, then return a list of compliance issues:

// app/api/analyze/route.ts
import { Ollama } from '@ollama/ai';
import { db } from '@/lib/db';
import { rules } from '@/lib/schema';

const ollama = new Ollama({ host: process.env.OLLAMA_HOST });

export async function POST(request: Request) {
  const { pageContent, url } = await request.json();

  // Generate embedding for the page content to find relevant rules
  const embeddingResponse = await ollama.embed({
    model: 'nomic-embed-text',
    prompt: pageContent,
  });
  const pageEmbedding = embeddingResponse.embeddings[0];

  // Find top 10 most relevant accessibility rules via pgvector similarity search
  const relevantRules = await db.query.rules.findMany({
    orderBy: (rules, { sql }) => [sql`${rules.embedding} <=> ${pageEmbedding}::vector`],
    limit: 10,
  });

  // Send rules and page content to local LLM for analysis
  const analysisResponse = await ollama.chat({
    model: 'llama3:8b-instruct-accessibility',
    messages: [
      {
        role: 'system',
        content: "You are an accessibility compliance expert for Indian public-facing web apps. Analyze the provided page content against the given GIGW 3.0 and WCAG 2.2 rules. Return a JSON array of issues, each with: severity (critical/warning/info), ruleName, ruleSource (GIGW 3.0 or WCAG 2.2), description, and suggestedFix. Only return valid JSON, no extra text.",
      },
      {
        role: 'user',
        content: `Page URL: ${url}\nPage Content: ${pageContent}\nRelevant Rules: ${JSON.stringify(relevantRules.map(r => r.ruleText))}`,
      },
    ],
    format: 'json',
  });

  return Response.json(JSON.parse(analysisResponse.message.content));
}

All processing happens on your local server—no page content or user data is ever sent to external providers.

Step 3: Set Up pgvector for Semantic Rule Matching

Instead of hardcoding hundreds of accessibility rules, we store embeddings of GIGW 3.0 and WCAG 2.2 rules in pgvector for fast, context-aware matching. First, define your database schema with Drizzle:

// lib/schema.ts
import { pgTable, text, vector, varchar } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

export const rules = pgTable('accessibility_rules', {
  id: varchar('id').primaryKey().default(sql`gen_random_uuid()`),
  ruleText: text('rule_text').notNull(),
  embedding: vector('embedding', { dimensions: 1536 }).notNull(),
  source: varchar('source', { enum: ['GIGW 3.0', 'WCAG 2.2'] }).notNull(),
  severity: varchar('severity', { enum: ['critical', 'warning', 'info'] }).notNull(),
});

Next, seed your database with the full set of GIGW 3.0 and WCAG 2.2 rules. Use the local nomic-embed-text model to generate embeddings, so no external API calls are needed:

// lib/seed-rules.ts
import { db } from './db';
import { rules } from './schema';
import { Ollama } from '@ollama/ai';

const ollama = new Ollama({ host: process.env.OLLAMA_HOST });

const gigwRules = [
  "All public-facing pages must have skip navigation links for keyboard users",
  "All form fields must have associated ARIA labels in English and the relevant regional language",
  "Color contrast between text and background must meet a minimum 4.5:1 ratio for normal text",
  // Add full GIGW 3.0 rule set here
];

const wcagRules = [
  "All non-decorative images must have descriptive alt text",
  "All interactive elements must be keyboard accessible",
  "Pages must have a logical heading hierarchy (h1-h6)",
  // Add full WCAG 2.2 rule set here
];

async function seed() {
  for (const rule of [...gigwRules, ...wcagRules]) {
    const embeddingResponse = await ollama.embed({
      model: 'nomic-embed-text',
      prompt: rule,
    });
    await db.insert(rules).values({
      ruleText: rule,
      embedding: embeddingResponse.embeddings[0],
      source: rule.startsWith('All public') ? 'GIGW 3.0' : 'WCAG 2.2',
      severity: rule.includes('must') ? 'critical' : 'warning',
    });
  }
  console.log('Rules seeded successfully');
}

seed();

Step 4: Build an Accessible Frontend

Your checker’s UI must itself be WCAG 2.2 AA compliant to set a standard for the apps it evaluates. Use Next.js App Router with React Server Components for fast initial loads, and a client component for the URL input form:

// app/page.tsx
import UrlInput from './url-input';
import ResultsDisplay from './results-display';

export default function Home() {
  return (
    <main className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-4xl mx-auto">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">
          GIGW 3.0 & WCAG 2.2 Accessibility Compliance Checker
        </h1>
        <p className="text-gray-600 mb-8">
          Self-hosted, privacy-first compliance analysis for Indian public web apps
        </p>
        <UrlInput />
        <ResultsDisplay />
      </div>
    </main>
  );
}

The UrlInput component uses proper ARIA labels, keyboard navigation, and error messaging to meet accessibility standards:

// app/url-input.tsx
'use client';

import { useState } from 'react';

export default function UrlInput() {
  const [url, setUrl] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    // Add fetch call to your /api/analyze route here
    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit} className="mb-8">
      <label htmlFor="url-input" className="block text-sm font-medium text-gray-700 mb-2">
        Enter public web app URL to scan
      </label>
      <div className="flex gap-4">
        <input
          id="url-input"
          type="url"
          value={url}
          onChange={(e) => setUrl(e.target.value)}
          required
          className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
          aria-describedby="url-error"
        />
        <button
          type="submit"
          disabled={loading}
          className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-blue-300"
        >
          {loading ? 'Scanning...' : 'Run Compliance Check'}
        </button>
      </div>
      {error && (
        <p id="url-error" className="mt-2 text-sm text-red-600" role="alert">
          {error}
        </p>
      )}
    </form>
  );
}

The results display groups issues by severity, links to the full GIGW 3.0 or WCAG 2.2 rule, and includes suggested fixes for developers.

Step 5: Harden Privacy and Security

To meet public sector security requirements, add these hardening steps:

  • Isolate Ollama: Run Ollama in a Docker container with no internet access for air-gapped deployments
  • Encrypt data at rest: Enable PostgreSQL encryption for stored scan logs and rule sets
  • Authenticate API routes: Add OAuth2 or SSO authentication for authorized compliance teams only
  • Sanitize inputs: Validate and sanitize all user-submitted URLs to prevent SSRF and XSS attacks
  • Rate limiting: Add rate limiting to API routes to prevent abuse
  • No telemetry: Disable all Next.js and Ollama telemetry to ensure no usage data leaves your server

Step 6: Test with Real Indian Public App Use Cases

Test your checker with common public sector page types to validate GIGW 3.0 support:

  • Aadhaar enrollment forms: Check for missing regional language labels and ARIA support
  • State government service portals: Validate color contrast for regional language text and skip navigation links
  • Public health dashboards: Check alt text for regional language infographics and data table accessibility

The local LLM will flag GIGW-specific issues that generic global checkers miss, like missing support for Devanagari script contrast ratios or regional language form labels.

Deployment for 2026 Public Sector Use

Deploy the entire stack via Docker Compose for consistent, reproducible deployments across government on-prem servers or India’s MeghRaj cloud:

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:password@db:5432/accessibility_checker
      - OLLAMA_HOST=http://ollama:11434
    depends_on:
      - db
      - ollama

  db:
    image: pgvector/pgvector:pg16
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=accessibility_checker
    volumes:
      - pgdata:/var/lib/postgresql/data

  ollama:
    image: ollama/ollama:latest
    volumes:
      - ollama-models:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu] # Optional, for faster LLM inference

volumes:
  pgdata:
  ollama-models:

A standard 8-core, 32GB RAM server can run the entire stack for thousands of daily scans, at a fraction of the cost of enterprise SaaS accessibility tools.

Conclusion

This self-hosted, privacy-first accessibility checker gives Indian public sector teams full control over their compliance workflow, with built-in support for GIGW 3.0 and WCAG 2.2, zero third-party data sharing, and no recurring costs. The entire codebase is open source, and Agentic Academy Labs offers a full hands-on course with pre-configured lab environments to help you build and deploy the tool for your team in 2026.

Enjoyed this article?

Read More Articles