Canada Mortgage Compare

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
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 — 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 — a wasted run.

The pipeline commits only if the generated data actually changed, since every commit costs a deploy.

Technical Implementation

Canadian mortgage math

This is the part most tools get wrong. Canadian fixed mortgages compound semi-annually under the Interest Act, not monthly as in the US. The periodic rate is derived accordingly:

code
i = (1 + j/2)^(2/n) − 1

decimal.js carries the arithmetic to avoid floating-point drift. Six payment frequencies are supported, and accelerated bi-weekly and weekly derive from the monthly payment divided by 2 or 4 — modelling what lenders actually do, rather than solving for an exact amortization that no lender offers. Payoff inside the term is handled so total cost isn't overstated. The one documented approximation: variable rates receive the same semi-annual treatment.

History as a public record

Rate history is committed JSON, capped at 2,000 entries, tracking product_added, rate_changed, terms_changed, and product_removed. Two integrity rules stand out:

*

A failed or disabled collector emits no entries at all. Inferring product_removed from a parser failure would put a lie in the public record.

*

New products are dated to the lender's own effective date, not the collection run, so a first run doesn't read as "everything changed at once."

A filter that hides itself

Every current product is nationally available. Rather than show a province filter that implies a distinction the data can't support, the filter hides itself — and returns automatically the moment a province-limited lender is added.

No chart library

Rate history renders as hand-written SVG. As the component notes, a single line chart does not justify shipping a chart bundle.

Tested against real fixtures

Eleven Vitest suites run against captured HTML fixtures for each lender, so a lender's redesign surfaces as a test failure rather than as bad data on the live site.

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)

PortalContact