Project Case Study
Canada Mortgage Compare
An independent reference site comparing advertised mortgage rates from five major Canadian lenders, refreshed twice daily. A pure static Next.js export with no database and no backend: rates are collected by TypeScript scrapers in GitHub Actions and committed as JSON.
View Live Project →Overview
Canada Mortgage Compare aggregates advertised mortgage rates from major Canadian lenders and presents them side by side. The hero states the scope without embellishment: "Compare advertised Canadian mortgage rates."
It currently tracks 64 products across five lenders (CIBC, National Bank of Canada, RBC, Scotiabank, and TD), refreshed twice daily.
The defining constraint is what the project refuses to be. There is no database, no backend, and no paid service. The entire site is a static Next.js export served from Cloudflare Pages, and the rate data lives as committed JSON in the repository.
The Honesty Problem
Rate comparison sites have a structural credibility problem: they are usually lead-generation funnels dressed as reference material. This project takes the opposite position, and enforces it in code.
Advertised rates are not offers. Your actual rate depends on credit, income, down payment, and the property. The site says so rather than implying otherwise.
No `Offer`/`Product` structured data. SEO markup is limited to Organization, WebSite, and BreadcrumbList. Emitting Offer markup for a rate that isn't guaranteed would, as the code puts it, be a lie in a machine-readable format, which is worse than one in prose.
BMO is registered but disabled, because it needs permission before it can be enabled. No BMO rates are published.
Architecture
No database, on purpose
Rates change twice a day and the dataset is small. A database would add an operational surface with nothing to show for it, so the pipeline is: collectors run in CI → validate → commit JSON → the commit triggers a Cloudflare Pages deploy.
next.config.ts sets output: "export" with trailingSlash: true. Static export forbids SSR, ISR, route handlers, and server actions, so the build fails loudly if anyone adds one. The constraint enforces itself.
Collectors
Custom TypeScript collectors live in collectors/, one per lender, registered in a central registry. Cheerio parses the HTML; Playwright is available but installs conditionally, and currently never runs, because every enabled collector works over plain fetch.
Guardrails protect against the failure mode that matters: a lender redesigns their page and a selector latches onto the wrong cell:
Rates must fall within a 0.5–15% band
A jump larger than 1.5 percentage points is treated as an anomaly
30-second timeout, 1.5-second polite delay, one anomaly retry
Scheduling that respects timezones
The GitHub Actions cron is 0 12,21 * * *: twice daily, but deliberately 9 and 15 hours apart rather than evenly split. 12:00 UTC is 08:00 in Toronto, catching overnight moves at open; 21:00 UTC is 17:00 Toronto, after close and 14:00 in Vancouver. An earlier 0 6,18 schedule was rejected because 06:00 UTC is 02:00 in Toronto, which would be a wasted run.
The pipeline commits only if the generated data actually changed, since every commit costs a deploy.
What This Means for the Business
Payments that match how lenders calculate: Figures follow Canada's semi-annual compounding rule and the way lenders actually set accelerated payments, so borrowers see realistic numbers instead of US-style estimates.
A rate history you can trust: Only real changes are recorded, and new products are dated by the lender's own effective date, so the history never shows false removals or changes that didn't happen.
No misleading options: Filters appear only when the data supports them; the province filter returns automatically once a lender offers province-limited products.
Problems caught before visitors see them: Each lender's pages are tested automatically, so a lender's website redesign is flagged before bad data reaches the live site.
Lean and low-cost to run: The site runs with no database, no backend, and no paid services, and the rate chart is drawn without adding a heavy charting add-on.
What This Case Study Demonstrates
Choosing the smallest architecture that fits the problem, and letting the build enforce it
Treating data integrity as an editorial responsibility, not just a correctness concern
Getting domain math right where the obvious implementation is wrong
Building a resilient scraping pipeline with guardrails, fixtures, and honest failure behaviour
Running a real data product at effectively zero infrastructure cost
Live: https://canadamortgage.servith.com/
Role: Solo delivery (collectors → mortgage math → integrity rules → UI → CI)