Gazzali Zayn GAZZALI ZAYN
Case Study · Full-Stack + AI · Legal-tech / Procurement

TenderAI — automating tender deviation review.

Live demo · shipped solo

A full-stack AI platform that automates legal review of construction & infrastructure tenders — flagging where a draft contract deviates from an approved standard, and drafting clause-anchored responses to bidders' pre-bid queries. It turns a multi-day, manual side-by-side document review into a minutes-long, auditable workflow — with a human approving every AI-drafted line.

Modules
2analysis + query drafter
Review time
days→minhuman approves each draft
Gemini keys
3-keyrotation beats free-tier limits
UAT ↔ PROD
1 vardispatcher pattern, parity
01 · The problem

Two long PDFs, read clause by clause.

In public-sector and infrastructure procurement, every draft tender must be checked against an authoritative standard contract (GCC 2023, CPWD, MoRTH, PSU conditions). Legal teams do this by hand: reading two long documents side by side, logging every deviation — added, removed, weakened, or risky — and responding to dozens of bidder pre-bid queries, each citing the exact governing clause.

It's slow, inconsistent between reviewers, and error-prone — and a single missed deviation can mean real contractual and financial exposure. Goal: compress this into an AI-assisted workflow that produces a structured, auditable deviation register and draft responses, while keeping a human firmly in the loop for approval.

02 · What I built

A two-module platform.

Module 1 — Deviation Analysis

Upload a standard document + a draft tender. The system extracts text from both, prompts Gemini to compare them, and returns a structured JSON deviation register — each entry classifying the deviation, its severity, and the governing clause.

Module 2 — Pre-Bid Query Drafter

Upload a bidder's query document. The system:

  • Extracts each discrete query (Gemini parse step)
  • Drafts a clause-anchored response citing both standard and draft
  • Flags risk — auto-tags legal_risk / needs_review / low_confidence
  • Lets a reviewer edit / approve / redraft
  • Exports the approved register to a formatted .docx

Supporting: a Master-Document library (platform standards + private reusable user uploads), a unified "recent work" dashboard across both modules, and JWT auth shared across the Next.js frontend and FastAPI backend.

03 · Tech stack

The build.

FrontendNext.js (React), edge-compatible JWT auth
BackendFastAPI (Python 3.11), async request handling
AIGoogle Gemini (gemini-2.5-flash) via REST, structured-JSON prompting
DatabasePostgreSQL (local UAT) / Supabase (production)
File storageLocal filesystem (UAT) / AWS S3 with presigned uploads (prod)
AuthJWT (python-jose), shared secret between frontend & backend
Document I/OPyMuPDF (PDF extraction), python-docx (Word export)
InfraRender (CI/CD on push to main), environment-based config
04 · Architecture

One env var drives the whole split.

The core pattern: a single variable, APP_ENV (uat | prod), drives the entire environment split. Three thin dispatcher modules — ai_service, db_service, storage_service — each inspect APP_ENV and delegate to the right implementation:

ConcernUAT (local dev)PROD (cloud)
DatabaseLocal PostgreSQLSupabase
File storage./uploads/AWS S3 (presigned URLs)
CORS originlocalhost:3000production frontend domain

Business logic stayed identical across environments, local development was fully offline-capable (no cloud dependency to write a feature), and production parity became a config change, not a code branch.

~/tenderai / request flow
Browser —JWT—> Next.js —Bearer/REST—> FastAPI
└─ require_auth routers: M1 analysis · M2 pre-bid · masters · dashboard
   └─ dispatchers { ai · db · storage }  switch on APP_ENV
      ├─ uat  → local Postgres + ./uploads
      ├─ prod → Supabase + AWS S3
      └─ ai   → Gemini  3-key rotation + tiered backoff
// M2 runs extract → draft as a background task (non-blocking)
05 · Engineering challenges

The problems that took thought.

a · LLM output truncation

Silent, invalid JSON on long documents

gemini-2.5-flash shares one token budget between internal "thinking" tokens and visible output. On long docs this silently ate the output budget and returned truncated, invalid JSON (finish_reason=MAX_TOKENS) — the deviation register simply failed to parse.

Fix: explicitly set thinkingBudget: 0 and request the full maxOutputTokens (65,536), plus a verification step (assert finish_reason=STOP) in the test loop so truncation regressions are caught before deploy.

b · rate limits

Surviving free-tier caps with multi-key rotation

Gemini's free tier caps requests per project. Under real use this throttled analyses mid-job. Fix: _call_gemini() tries up to three API keys, each in a separate Google project (each with its own fresh quota). On a 429/503 it fails over immediately, then applies tiered backoff (20/40/60s for quota, 2/4/6s for transient overload). One shared call path means all three modules benefit at once.

c · debugging depth

A "CORS error" that wasn't a CORS error

A prod bug surfaced as a browser CORS failure. The real cause: prebid_* tables use a text PRIMARY KEY with no DB default; the Supabase insert path wasn't generating an ID, so the insert hit a not-null violation, the unhandled 500 returned without CORS headers, and the browser reported it as a CORS block.

Fix & lesson: generate the UUID in app code on both DB paths, and wrap the endpoint so failures return a clean, CORS-bearing error. Captured as a convention: a missing-CORS-header symptom usually means an unhandled 500 underneath.

d · async · e · small bugs · f · secrets

The rest of the iceberg

Long-running AI (M2's two sequential Gemini calls) runs as background tasks; blocking httpx calls wrapped in asyncio.to_thread() to avoid stalling the event loop. Small bugs, outsized impact: .docx downloads failed on tender names with em-dashes (Content-Disposition must be latin-1) — fixed by sanitising to ASCII; the drafter prompt initially flagged nearly everything for review — tuned to flag only genuine legal risk. Secrets hygiene: env files git-ignored, a codified read-only-by-default rule for production, and a documented Render env-var foot-gun (PUT replaces all variables).

06 · Outcomes

AI accelerates; humans decide.

Speed

A manual side-by-side tender review compressed from a multi-day task into a minutes-long, reviewable workflow.

Trust by design

A consistent, auditable deviation register with human approval gating every AI-drafted response — risk flags, citations, and an edit/approve loop before anything is exported.

Shipped solo, end to end

Backend, AI integration, cloud infra, and the deployment pipeline — all built and deployed by one person.

Resilient by default

Multi-key rotation, structured-output validation, and background processing keep it working under free-tier limits and long documents.

07 · Skills demonstrated

What it shows.

AI / LLMStructured-output prompting, token-budget management, prompt tuning, resilient multi-key rate-limit handling, human-in-the-loop review design.
BackendFastAPI, async patterns, background jobs, JWT auth, clean environment-abstraction (dispatcher pattern), migrations.
Cloud / DevOpsAWS S3 presigned uploads, Supabase, Render CI/CD, environment parity, production-safety discipline.
ProductScoped a real legal/procurement workflow into two shippable modules; prioritised reviewer trust over full automation.
DebuggingDiagnosed misleading symptoms (CORS-masked 500s, silent LLM truncation) to their true root causes.
08 · What I'd do next

The roadmap.

Billing-enabled Gemini

Move from free tier to Tier 1 for higher limits and to disable prompt-data training — required before handling real client tenders at scale.

Clause-level diff UI

Anchor each deviation to its source text spans, with inline highlighting in the document viewer.

Evaluation harnesses

Precision/recall on a labelled set of known deviations, to catch prompt/model regressions before they ship.

Multi-tenant access

Replace demo credentials with proper user management and role-based access control.

See it live.

Try the deployed app, or read the other case studies.