Countworthy

Home/Security

Security and privacy practices

Written for whoever has to sign off on putting a Countworthy calculator on an institutional site. Every claim below is provable against this project’s own configuration and code — nothing here is a policy statement standing in for something we haven’t built.

What runs in the visitor’s browser

Everything. Every calculator on this site — TFSA room, RRSP refund, GIC growth, take-home pay, and the rest — computes with plain JavaScript running locally, against a shared, sourced tax or rates engine (ca-tax-2026.js, ca-land-transfer-2026.js, ca-mortgage-2026.js, ca-gic-2026.js). A calculator never sends what a visitor types to us, to a host site, or to anywhere else — there is no fetch, no form submission and no beacon carrying an input value. Any state a calculator keeps is encoded only in the page’s own URL (share.js’s “Copy link to this scenario”), so a shared link can restore a scenario without anything ever having been stored on a server. Every /embed/*.html widget is a self-contained page: it loads its own engine script and Google Fonts and nothing else — no analytics, no tracking pixel, no third-party call of any kind.

What is stored

Nothing you type into a calculator. The site keeps exactly one item in the visitor’s own browser: country.js writes a single localStorage key (the country you picked in the header) so the site does not ask again on your next visit — never sent anywhere, cleared by clearing this site’s data. The only thing that reaches our database is an opt-in comparison tally (stats.js): a visitor who ticks “Compare me anonymously” sends a province code and a wide income band — never the figure they typed, never a name, session or timestamp — and that adds 1 to a shared public counter. See the full explanation on our privacy page.

Where things are hosted

Security headers, as served

Quoted from firebase.json, not summarised. These apply to every page on the site:

Applied to every page
HeaderValue
Content-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://firestore.googleapis.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'
X-Frame-OptionsDENY
Strict-Transport-Securitymax-age=31536000; includeSubDomains
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
X-Content-Type-Optionsnosniff
Cache-Control (HTML)no-cache, must-revalidate
Cache-Control (css/svg/png/ico/woff2)public, max-age=604800
Cache-Control (js)public, max-age=3600

The /embed/** widgets carry different values, because they exist to be framed by someone else’s site — these three override the site-wide ones above for that path only:

Applied to /embed/** only
HeaderValue
Content-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; frame-ancestors *; form-action 'none'; base-uri 'self'; object-src 'none'
X-Frame-OptionsALLOWALL
Cache-Controlpublic, max-age=3600

The rest of the site’s headers (HSTS, Referrer-Policy, Permissions-Policy, X-Content-Type-Options) still apply to /embed/**; only the three above are overridden, because a page meant to sit inside someone else’s <iframe> cannot also refuse to be framed.

Firestore access rules

The counter database’s security rules permit exactly one write from a browser: create a counter document at n: 1, or increment an existing one by exactly 1 — never a decrement, an overwrite, an arbitrary field, or a delete. Reading the aggregate counts back is public, because the published tallies are the dataset itself. Every other path is closed by a default-deny rule. The rules file is quoted here in full, so nothing has to be taken on trust.

firestore.rules, as deployed
rules_version = '2';

// Countworthy — aggregate statistics only.
//
// THE WHOLE DATABASE IS COUNTERS. There is no user, no session, no submission row and no
// document that describes a person. A browser that opts in computes a coarse bucket locally
// (province + a wide band, never a typed figure) and adds exactly 1 to one shared tally.
// Nothing here can be traced back to anybody, because nothing about anybody is written.
//
// Document shape: aggregates/{calc}/buckets/{region__band}  ->  { n: <int> }
// Example:        aggregates/ca-take-home-2026/buckets/ON__060-070k  ->  { n: 412 }
//
// The rules below are what makes the privacy promise structural rather than a policy: a client
// literally cannot write anything except "one more person landed in this band", and cannot
// decrement, overwrite, delete, or add a field.
service cloud.firestore {
  match /databases/{database}/documents {

    match /aggregates/{calc}/buckets/{bucket} {

      // The tallies are public. They are the dataset we publish.
      allow read: if true;

      // First person ever in this band: the counter starts at exactly 1.
      allow create: if isOnlyCount() && request.resource.data.n == 1;

      // Everyone after: exactly one more than what was there. Not two, not minus one.
      // Firestore evaluates rules against the post-transform document, so a client using an
      // increment(1) transform satisfies this and an increment(1000) does not.
      allow update: if isOnlyCount() && request.resource.data.n == resource.data.n + 1;

      // Counters are never removed by a visitor.
      allow delete: if false;
    }

    // Everything else is closed. If a future feature needs storage it gets its own rule,
    // written deliberately, rather than inheriting a permissive default.
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

function isOnlyCount() {
  return request.resource.data.keys().hasOnly(['n'])
      && request.resource.data.n is int
      && request.resource.data.n > 0
      && request.resource.data.n < 1000000000;
}

What we do not have and do not claim

We do not have a SOC 2 report, an ISO 27001 certification, or a penetration-test attestation. We do not have user accounts of any kind — there is nothing to authenticate, so there is no account database to compromise. We do not claim a PIPEDA cross-border-transfer assessment for personal information, because there is no personal information collected to transfer: every calculator input stays in the visitor’s browser, and the one thing that does reach our database — the opt-in comparison tally — is anonymous by construction, not merely anonymised after the fact, so there is no personal record on either side of any border to assess. The Privacy Commissioner of Canada's own guidelines for processing personal data across borders frame that whole assessment around a “transfer” of personal information to a third party for processing — a “use,” protected by contract, that the transferring organization stays accountable for. That framework has nothing to attach to here, not because an exception excuses us, but because the precondition it assumes — personal information changing hands — never occurs. If your review requires a specific certification we do not hold, the honest answer is that we do not hold it.

How this maps to your own third-party review

Whether your institution answers to a provincial regulator like Ontario's FSRA or, for the small number of federally regulated credit unions, to OSFI under Guideline B-10, the underlying doctrine is the same: your institution keeps accountability for anything it embeds, and the diligence it owes scales with the arrangement's risk and criticality rather than a fixed checklist applied identically to every vendor. FSRA's IT Risk Management Guidance (No. GR0016INT) requires that outsourcing risk be “properly identified, assessed, and managed” while “accountability and ownership for any outsourced or co-sourced function is maintained by” the institution, never by us. OSFI's Guideline B-10 is more explicit still: it applies “in a manner that is proportionate to … the risk and criticality of each third-party arrangement,” and notes that “an exit or contingency plan may not be needed for a low-risk arrangement” and “a legal review may not be necessary for a low-risk, short-term arrangement.” A read-only widget that touches no member data and carries no ongoing service obligation is exactly the kind of arrangement both frameworks describe as warranting a lighter review than a core-banking or payments vendor — not a reason to skip the review, but a reason it can be short.

The table below maps the categories a Canadian credit-union third-party questionnaire actually asks about — drawn from the Credit Union Deposit Guarantee Corporation's (Saskatchewan) Third-Party Risk Management Guideline (2025-01), whose Appendix A due-diligence list and Appendix B contract provisions are representative of what a provincial credit-union regulator expects a member to ask — onto what this page answers, declines honestly, or is silent on today.

What a vendor questionnaire asks, answered plainly
CategoryWhere this stands
SubprocessorsAnswered in full above (“Where things are hosted”): Google (Firebase Hosting and Firestore), Google Fonts, and Cloudflare Email Routing for our own inbox — nothing else touches this site.
Data locationAnswered above: the counter database is in northamerica-northeast2 (Toronto, Canada); no personal data exists to locate anywhere else.
Access controlNot published on this page today.
Change management & testingAnswered: every figure carries a dated citation, a second-person re-derivation and a runnable Node test suite that fails the build when a page passes its own re-verification date — see Methodology.
Incident responseNot documented as a formal playbook today — ask us if a written notification commitment is a condition of your approval.
Business continuity / insuranceNot published today. We are a two-person team and have not disclosed a continuity plan or an insurance policy for the free embed; a paid white-label build (see Countworthy for Credit Unions) comes with written terms you can negotiate these into.
Contract & audit rightsThe free embed carries no contract or SLA, by design — there is nothing to enforce against a page that computes locally and stores nothing. A paid build is a written agreement; ask about subcontracting, audit and exit clauses if your policy requires them in writing.

We would rather a cell here say “not published” than have your reviewer discover the gap themselves after approval. Every unanswered row above is a real open question, not an oversight we hope you miss.

Accessibility

Every calculator is built to keyboard-complete operation, with a labelled control for every input and a single live region announcing the result once per change rather than flooding a screen reader with every keystroke. We have not commissioned a formal WCAG audit and do not claim certification against any accessibility standard — these pages are built to that bar, not certified against it.

For an Ontario reviewer specifically: the Integrated Accessibility Standards Regulation (O. Reg. 191/11 under the AODA) requires “large organizations” — those with 50 or more employees in Ontario — and every designated public-sector body to conform their public web content to WCAG 2.0 Level AA. That obligation is yours, not ours, and under the regulation it extends to whatever your own site carries, including a third party's embedded widget. Separately, and relevant right now: 2026 is itself a filing year — organizations with 20 or more Ontario employees must submit a self-attested accessibility compliance report by 31 December 2026. There is no such thing as “AODA-certified” third-party content — the Act creates no certification and no body issues one — so “built to” is the honest framing for any vendor's claim here, not a euphemism for something weaker. If your compliance report needs a signed conformance statement (a VPAT or equivalent) for this specific widget, ask — we do not have one today, and would rather say so than let you assume otherwise.

Who to ask

Security questions, or something specific your review needs that isn’t answered above: hello@countworthy.com.

This page describes the system as built on 10 September 2026. If the stack changes — a new header, a different host, a new place data is stored — this page changes in the same commit or it stops being true.