BeforeYouSign is a Next.js web app that helps renters review residential lease documents before signing. Users upload a PDF lease, paste plain text, or load sample lease fixtures. The server extracts text (when needed), runs regex-based clause detection and deterministic risk scoring, and optionally calls Google Gemini to produce a structured JSON report (summary, fees, deadlines, red flags, suggested questions). Results display in the browser with evidence quotes tied to page-level extracted text.
Lease agreements are long and written in dense legal language. Renters often struggle to quickly spot costs, notice and renewal rules, maintenance or utility responsibilities, and clauses worth questioning. This project reduces that friction by highlighting likely pressure points and summarizing them in plain language, while staying informational (not legal advice).
- PDF lease upload with server-side text extraction (
pdf-parse), plus paste text and built-in sample leases frompublic/sample-leases/. - Structured report UI: carousel sections for summary, red flags, money and fees, deadlines, responsibilities, questions, next steps, and “not clearly stated” when applicable.
- Evidence linking: clicking report rows can scroll the extracted text viewer and highlight matching quotes by page.
- Rule-based snippet extraction (rent, deposit, fees, notice, renewal, maintenance, utilities, vague phrases) and deterministic risk band with reasons.
- Gemini-powered narrative report with JSON schema validation; fallback report built from rules when the model fails or returns invalid JSON.
- Transparency panel (“How this was analyzed”) showing extraction counts, snippet hits, and heuristic risk signals.
Frontend: Next.js (App Router), React, TypeScript, Tailwind CSS v4, Embla Carousel, Lucide icons, shadcn-style UI primitives (src/components/ui/).
Backend: Next.js Route Handler — single endpoint POST /api/analyze (src/app/api/analyze/route.ts), Node runtime.
AI/API: Google Gemini via @google/generative-ai (src/lib/analysis/gemini-report.ts), structured output aligned with src/lib/analysis/schema.ts.
Other tools: pdf-parse + pdf-lib (src/lib/pdf/), ESLint (npm run lint), Playwright QA smoke scripts.
git clone https://github.com/ChimdumebiNebolisa/BeforeYouSign.git
cd BeforeYouSignnpm installCreate a .env.local file in the root (you can start from .env.local.example):
Windows (cmd): copy .env.local.example .env.local
macOS / Linux: cp .env.local.example .env.local
BYS_AI_KEY=
BYS_GEMINI_MODEL=gemini-2.5-flashEnvironment variables used:
BYS_AI_KEY: Google AI API key for Gemini (server-side only; never use NEXT_PUBLIC_).
BYS_GEMINI_MODEL: Optional model id; defaults to gemini-2.5-flash if unset.Optional for development:
set BEFOREYOUSIGN_PDF_DEBUG=1(Unix: export BEFOREYOUSIGN_PDF_DEBUG=1) — enables extra PDF extraction logging on the server.
npm run devOpen the local URL shown in the terminal (typically http://localhost:3000).
For this project:
- Choose intake: Upload a PDF, paste lease text, or load a sample file from the UI (
src/components/beforeyousign/). - Submit analysis: The client sends
POST /api/analyze— multipart (file) for PDFs or JSON (leaseText, optionalfileName) for pasted/sample text (landing-client.tsx,route.ts). - Prepare text: PDFs are read per page via
extractPdfTextPages; pasted text becomes a single synthetic page. All text passesnormalizeLeasePageText(src/lib/pdf/). - Deterministic pass:
rules.tsextracts snippet matches;scoring.tscomputes a risk band and reasons; ambiguous phrases are flagged (findUnclearLeasePhrases). - AI report (when configured):
runStructuredLeaseAnalysiscalls Gemini withbuildLeaseAnalysisUserPrompt; responses are parsed (model-json.ts), validated (parseBeforeYouSignReportJson), and normalized (report-normalization.ts). On failure,buildRuleOnlyFallbackReportsupplies a report-shaped fallback (route.ts). - Render results: JSON returns
extractedPages, snippet arrays, deterministic risk fields, andreport. The client showsLeaseTextViewer,LeaseReportView, andTechnicalDetailsPanel(landing-client.tsx).
Brief folder layout:
src/app/: App Router — layout, page, favicon/app icons, POST /api/analyze route.
src/components/beforeyousign/: Intake, report carousel, text viewer, loading shell.
src/components/ui/: Shared UI (e.g. Button).
src/lib/analysis/: Regex rules, Gemini, schema, scoring, prompts, normalization.
src/lib/pdf/: PDF extraction (pdf-parse) and text normalization.
public/: Static assets — sample leases, images.System overview:
Frontend: React client components; single-page lease intake and results.
Backend: Next.js Route Handler (Node) — one analyze endpoint; no separate API server.
External services: Google Gemini API (when BYS_AI_KEY is set).
Deployment: Standard Next.js production build (npm run build && npm run start); host per your platform (e.g. Vercel-compatible).High-level request flow — there is no database; results live in client state after the response.
flowchart TB
subgraph Client["Browser — React"]
LC["LandingClient — intake"]
V["LeaseTextViewer"]
R["LeaseReportView"]
T["TechnicalDetailsPanel"]
LC --> V
LC --> R
LC --> T
end
subgraph Route["POST /api/analyze — src/app/api/analyze/route.ts"]
IN{"Body type?"}
PDF["extractPdfTextPages — pdf-parse"]
TXT["JSON leaseText → normalize → 1 synthetic page"]
NORM["normalizeLeasePageText"]
RULES["rules.ts — snippet finders"]
RISK["scoring.ts — deterministic band + reasons"]
GEM["gemini-report.ts — Gemini JSON schema"]
RNORM["report-normalization.ts"]
FALL["buildRuleOnlyFallbackReport"]
OUT["JSON — pages, snippets, risk, report"]
IN -->|multipart PDF| PDF
IN -->|application/json| TXT
PDF --> NORM
TXT --> NORM
NORM --> RULES
RULES --> RISK
RISK --> GEM
GEM -->|parsed OK| RNORM
GEM -->|timeout / parse / schema fail| FALL
RNORM --> OUT
FALL --> OUT
end
LC -->|"fetch POST"| IN
OUT -->|"response"| LC
flowchart LR
subgraph api["API route"]
RT["route.ts"]
end
subgraph pdf["PDF + text"]
EXT["pdf/extract-text.ts"]
NOR["pdf/normalize.ts"]
end
subgraph analysis["Analysis"]
RL["rules.ts"]
SC["scoring.ts"]
GR["gemini-report.ts"]
SCH["schema.ts"]
MR["model-json.ts"]
REP["report-normalization.ts"]
PR["prompt.ts"]
end
RT --> EXT
RT --> NOR
RT --> RL
RT --> SC
RT --> GR
GR --> SCH
GR --> MR
GR --> REP
GR --> PR
Notes
BYS_AI_KEY: If unset, the route still returns snippets and deterministic risk, butreportmay be null with a user-facingreportErrorstring instead of a Gemini-produced report.- Paste/sample text skips PDF extraction and is analyzed as a single virtual page.
Pull requests run the QA script syntax GitHub Actions workflow, which installs dependencies and verifies the committed QA scripts parse successfully:
node --check scripts/phase2-scan-smoke.mjs
node --check scripts/smoke-test.mjs
node --check scripts/phase1-browser-qa.mjs
node --check scripts/phase2-browser-qa.mjsRun static linting:
npm run lintRun the API scan smoke test against a running local server:
npm run dev
npm run smoke:scanRun browser QA smoke checks against a running local server. Install Playwright browser binaries first if you have not run browser QA on this machine:
npx playwright installnpm run qa:smoke
npm run qa:phase1
npm run qa:phase2The browser QA scripts use Playwright and write screenshots under qa-screenshots/ for visual review.
- No user accounts or persisted reports — refreshing loses in-session results unless the user runs analysis again.
- Single synchronous HTTP request — very large PDFs or slow model responses may hit hosting timeouts (
maxDurationon the route is capped for serverless-style deployments). - PDF text extraction is not OCR — scanned image-only PDFs may yield little or no extractable text.
- Evidence highlighting uses substring matching (
indexOfon the quote); minor mismatches between model quotes and extracted text can prevent a highlight. - Automated
npm testsuite is not present in this repo; use the QA script syntax workflow,npm run lint,npm run smoke:scan, and the Playwright QA scripts for repeatable checks.