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 →
Category
Data Product / Rate Comparison
Platform
Next.js
Client
SERVITH (Internal product)
Type
Web App (Static Data Product)
Role
Solo: collector architecture, mortgage math, data integrity rules, UI, and CI scheduling.
Goal
Let Canadian borrowers compare advertised mortgage rates across major lenders honestly, with the math correct and the limits of the data stated plainly.
Scope
Six static routes (compare, calculator, history, methodology, disclaimer, home), five lender collectors, and a twice-daily GitHub Actions pipeline.

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)

PortalGet a Free Quote