CMPUT-499 · Lesson 9 of 12 · 29 min

Data, privacy and responsible features

RescueRoute holds personal information about real people: volunteers' names, phone numbers, email addresses and availability, and records of where and when they picked up food. Some volunteers are retired, some are students, some are newcomers to the country, and a few have told Grace they do not want their phone numbers shared with anyone. The co-op also works with shelters whose clients are among the most vulnerable people in the city. Privacy and responsibility are not compliance afterthoughts for this project; they are part of whether the client can trust the product at all. CMPUT-331 covered privacy-enhancing technologies, CMPUT-421 research ethics and deceptive design, and AI-460 fairness and responsible AI. This lesson turns those ideas into concrete design decisions for a small system.

Privacy by design

Privacy by design, a framework developed by Ann Cavoukian when she was Ontario's Information and Privacy Commissioner in the 1990s, asks teams to build privacy in from the start rather than adding it later. Its principles translate into a short set of practical questions a capstone team can ask of every feature. What personal data does this feature need, and can it work with less? Who can see it, and why? How long do we keep it? How is it protected in transit, at rest and in logs? Can people see, correct and delete their own data? Is the default the most private reasonable option? Answering these in the user story, not after release, is what makes privacy cheap.

A flow of personal data through RescueRoute in five stages: collect, use, store, share and delete. Under each stage is the decision Team Relay made. Collect: only display name, one contact method and availability; no address, birth date or licence number. Use: contact details visible only to coordinators; drivers see first names. Store: encrypted in transit and at rest; no personal data in logs. Share: no third parties except the email and SMS providers under their terms. Delete: inactive drivers removed after 24 months; pickup records de-identified.
Privacy decisions at each stage of the data life cycle in RescueRoute

Data minimization

The safest personal data is data you never collect. Team Relay built a data inventory in week 5: a table listing every field the system stores, why it is needed, who can see it and how long it is kept. The first draft, based on the co-op's spreadsheet, included home addresses, dates of birth and driver's licence numbers. Asked why, Grace explained that the spreadsheet had columns for everything a previous coordinator had wanted. The co-op's insurance required that drivers hold a licence, but it did not require the co-op to store the number; a checkbox recording that the coordinator had seen the licence was enough. Addresses and birth dates had no current use at all. Removing them shrank the risk of the whole system.

Part of Team Relay's data inventory, agreed with the co-op
FieldPurposeWho can see itRetention
Display nameShow who claimed a shiftAll signed-in users (first name and initial)While active, plus 24 months
Email or phone (one required)Sign-in links and remindersThe volunteer and coordinatorsWhile active, plus 24 months
Reminder channel and consentSend reminders the way the volunteer choseThe volunteer and coordinatorsWhile active; consent changes logged
Availability patternSuggest suitable shiftsThe volunteer and coordinatorsWhile active
Licence seen (yes/no, date)Insurance requirementCoordinatorsWhile active, plus 24 months
Pickup recordsFunders' reports, operationsCoordinators and directorKept; driver link removed after 24 months
Removed: address, birth date, licence numberNo current purpose-Never collected

Tell people plainly what you collect and why, at the point of collection, and let them choose where a choice is real. RescueRoute's sign-up page has a short privacy notice in plain language, written with Grace, with a link to a fuller page. Volunteers choose email or text reminders, and text reminders require a separate, unticked checkbox with the wording 'Send me shift reminders by text message. Message and data rates may apply. Reply STOP to stop.' Consent for automated texts matters legally as well as ethically: in Canada, Canada's Anti-Spam Legislation, CASL, governs commercial electronic messages, and in the United States, the Telephone Consumer Protection Act, TCPA, regulates automated texts. Reminders a volunteer asked for are not marketing, but recording clear consent and honouring STOP replies is the safe and respectful design either way.

Avoid deceptive design. CMPUT-421 covered dark patterns: pre-ticked boxes, confusing double negatives, making it hard to opt out or delete an account. A volunteer organization has every reason to make leaving easy, because a volunteer who can leave easily is more likely to come back. RescueRoute has a 'Delete my account' button in settings that removes contact details immediately and de-identifies past pickups, with a confirmation step and no guilt-trip wording. Grace was initially worried this would lose volunteers; the team showed her that only two of the pilot drivers used it, both of whom had moved away.

Protecting the data

Security controls from lessons 5, 6 and 8 protect personal data: encryption in transit with HTTPS everywhere, encryption at rest provided by the managed database, role-based access so drivers cannot see other drivers' contact details, sign-in links that expire, an audit log of changes to shifts and accounts, and dependencies kept up to date. Two less obvious places leak personal data in many systems: logs and backups. Backups are handled by the managed database and are encrypted and expire on the provider's schedule, which the handover guide documents. Logs need the team's own discipline.

Logs are written by every part of the system, read by developers, shipped to monitoring services and kept for weeks. They are a common source of privacy breaches because developers log whole request objects or error messages that contain email addresses, phone numbers or tokens. Team Relay's rule was that logs identify records by internal ID, never by contact details, and that a redaction filter runs on every log line as a safety net. The filter is tested like any other code, including tests that confirm it does not mangle numbers that are not phone numbers, such as order IDs or years.

A log redaction filter for email addresses and phone numbers, with tests; all contact details are invented
# Keep personal data out of logs: redact email addresses and phone numbers before
# a log line is written. Tested on realistic lines (all addresses here are invented).
import re
EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
PHONE = re.compile(r"(?<!\d)(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}(?!\d)")
def redact(line):
    line = EMAIL.sub(lambda m: m.group(0)[0] + "***@" + m.group(0).split("@")[1], line)
    return PHONE.sub(lambda m: "***-***-" + re.sub(r"\D", "", m.group(0))[-4:], line)

lines = [
    "INFO reminder sent to ruth.p@example.org for shift 418",
    "INFO sms reminder queued for +1 204-555-0147 shift 422",
    "WARN bounce from omar.h@mail.example.net (mailbox full)",
    "INFO driver (414) 555-0199 replied CANCEL to shift 431",
    "INFO pickup 418 completed: 42.5 kg, order 2045550147 at donor 3",
    "ERROR login link expired for user id 77",
]
for l in lines:
    print(redact(l))
tests = [("call 204.555.0147 now", "call ***-***-0147 now"), ("a@b.co", "a***@b.co"), ("shift 2027", "shift 2027")]
print("redaction tests:", "all pass" if all(redact(i) == o for i, o in tests) else "FAIL")
Output
INFO reminder sent to r***@example.org for shift 418
INFO sms reminder queued for ***-***-0147 shift 422
WARN bounce from o***@mail.example.net (mailbox full)
INFO driver ***-***-0199 replied CANCEL to shift 431
INFO pickup 418 completed: 42.5 kg, order ***-***-0147 at donor 3
ERROR login link expired for user id 77
redaction tests: all pass

The output shows a subtle problem worth catching: the ten-digit order number in the fifth line looks exactly like a North American phone number and was redacted. That is a safe failure, since losing part of a log message is better than leaking a phone number, but it would confuse anyone debugging a donor's order. The team's response was to stop logging external order numbers in that message at all, since the pickup ID was enough to look them up. Redaction is a safety net, not a substitute for not logging personal data in the first place.

Retention and deletion

Keep personal data only as long as it is needed, and delete it on a schedule rather than when someone remembers. Team Relay and the co-op agreed a retention policy: contact details of drivers inactive for 24 months are deleted, and pickup records are kept for funders' reports but de-identified by removing the link to the deleted driver. A nightly background job applies the policy and writes an audit entry each time it runs, so the co-op can show that the policy is followed, not just written down. Deletion requests from volunteers are handled immediately through the account settings rather than waiting for the job.

Applying the 24-month retention policy: deleting inactive drivers and de-identifying their pickups while keeping totals reportable
# Retention: delete what you no longer need, on a schedule, and prove it ran.
# Policy agreed with the co-op: personal contact details of drivers inactive for
# 24 months are deleted; pickup records are kept for funder reports but de-identified.
from datetime import date
today = date(2027, 4, 12)
drivers = [
    # (id, name, contact, last active)
    (1, "Ruth P.", "ruth.p@example.org", date(2027, 4, 2)),
    (2, "Omar H.", "+1-555-0100", date(2025, 1, 15)),
    (3, "Mei L.", "mei@example.org", date(2025, 6, 30)),
    (4, "Jon K.", "+1-555-0177", date(2024, 11, 3)),
]
pickups = [(101, 2, 38.0), (102, 4, 55.5), (103, 1, 20.0), (104, 4, 12.0)]
def months_between(a, b): return (b.year - a.year) * 12 + (b.month - a.month)
expired = {d[0] for d in drivers if months_between(d[3], today) >= 24}
kept = [d for d in drivers if d[0] not in expired]
pickups = [(pid, None if drv in expired else drv, kg) for pid, drv, kg in pickups]
print("deleted driver records:", sorted(expired))
print("remaining drivers:", [d[1] for d in kept])
print("pickups after de-identification:", pickups)
print(f"total kg still reportable: {sum(kg for _, _, kg in pickups)}")
print("audit log: retention job ran", today.isoformat(), f"deleted={len(expired)}")
Output
deleted driver records: [2, 4]
remaining drivers: ['Ruth P.', 'Mei L.']
pickups after de-identification: [(101, None, 38.0), (102, None, 55.5), (103, 1, 20.0), (104, None, 12.0)]
total kg still reportable: 125.5
audit log: retention job ran 2027-04-12 deleted=2

Should RescueRoute use machine learning?

In sprint 2, Marcus proposed a machine learning model to predict which drivers were likely to miss a pickup, trained on the co-op's history, so the coordinator could be warned early. CMPUT-367 and CMPUT-466 gave the team the skills to build one. The team discussed it seriously, using the questions AI-460 taught. Was there enough data? About 750 pickups, of which roughly 60 were missed: too few for a reliable model, especially for new drivers with no history. What harm could a wrong prediction cause? A volunteer flagged as unreliable might be quietly given fewer shifts, a form of unfair treatment that is hard to see and hard to contest. Was there a simpler alternative? Yes: a confirmation request the day before, with an alert when a shift is unconfirmed 12 hours out, addresses the same problem transparently for every driver.

The team chose the simple rule, recorded the decision and its reasoning in an ADR, and noted in the handover guide that a predictive model might be reconsidered if the co-op ever had several years of data and a clear process for volunteers to see and challenge how they are treated. This is a good outcome, not a failure to use advanced skills. Responsible engineering includes deciding not to build something. If a capstone does use machine learning, it should come with an evaluation on held-out data, a check of performance across relevant groups, a model card describing intended use, data, metrics and limitations, and a plan for monitoring drift after handover, as AI-420 and AI-460 described.

Checking a feature for unequal treatment

Even without machine learning, ordinary rules can treat people unequally. The coordinator dashboard has a 'suggested drivers' list for unfilled shifts, ranked by each driver's completion rate. Before release, Lin asked two questions: do suggestions go disproportionately to some neighbourhoods, and are new drivers ever suggested? The check below simulates the co-op's driver pool. The naive ranking never suggests a new driver, because an unknown completion rate counts as zero, and it over-represents the area whose established drivers happen to have slightly higher rates. The team's first fix, giving new drivers a neutral starting score, was not enough, because established drivers' rates were all higher. The version they shipped reserves a few places for new drivers, rotated so each gets a turn, and fills the rest in proportion to each area's share of drivers. They also made the list a suggestion only, with the coordinator choosing freely, and showed the reason for each suggestion.

Comparing who gets suggested by a naive ranking and by an area-balanced list with places reserved for new drivers (simulated pool)
# Checking a "suggested drivers" feature for unequal treatment before release.
# The ranking prefers drivers with a high completion rate. Do suggestions go
# disproportionately to some areas of the city, and are new drivers ever suggested?
import random
random.seed(499)
areas = {"North": 40, "Central": 55, "South": 45}
drivers = []
for area, n in areas.items():
    for i in range(n):
        new = random.random() < 0.25
        # new drivers have no history: completion rate unknown -> 0 in the naive version
        rate = 0.0 if new else min(1.0, random.gauss(0.9 if area != "South" else 0.84, 0.06))
        drivers.append(dict(area=area, new=new, rate=rate))
def naive(drivers, k=20):
    return sorted(drivers, key=lambda d: d["rate"], reverse=True)[:k]
def balanced(drivers, k=20, new_slots=4):
    # 1) reserve a few places for new drivers (rotated by fewest shifts in the real system)
    by_area = [[d for d in drivers if d["new"] and d["area"] == a] for a in areas]
    new = [d for group in zip(*by_area) for d in group][:new_slots]   # round-robin across areas
    # 2) fill the rest in proportion to each area's share of established drivers
    est = [d for d in drivers if not d["new"]]
    out = list(new)
    for area in areas:
        pool = sorted((d for d in est if d["area"] == area), key=lambda d: d["rate"], reverse=True)
        out += pool[:round((k - new_slots) * len(pool) / len(est))]
    return out[:k]
for label, fn in (("naive ranking by completion rate", naive), ("area-balanced with 4 places for new drivers", balanced)):
    top = fn(drivers)
    print(label)
    for area, n in areas.items():
        share_pool = n / len(drivers)
        share_top = sum(d["area"] == area for d in top) / len(top)
        print(f"  {area:8} {100 * share_pool:4.0f}% of drivers, {100 * share_top:4.0f}% of suggestions")
    print(f"  new drivers suggested: {sum(d['new'] for d in top)} of {len(top)}")
Output
naive ranking by completion rate
  North      29% of drivers,   30% of suggestions
  Central    39% of drivers,   45% of suggestions
  South      32% of drivers,   25% of suggestions
  new drivers suggested: 0 of 20
area-balanced with 4 places for new drivers
  North      29% of drivers,   35% of suggestions
  Central    39% of drivers,   35% of suggestions
  South      32% of drivers,   30% of suggestions
  new drivers suggested: 4 of 20

The law, briefly

In Canada, the federal Personal Information Protection and Electronic Documents Act, PIPEDA, sets out fair information principles for organizations collecting personal information in commercial activities, and Alberta, British Columbia and Quebec have their own private-sector laws; Quebec's Law 25 is the most demanding, with privacy impact assessments and privacy-by-default requirements now in force. Many non-profits' core activities fall outside PIPEDA, and Manitoba has no general private-sector privacy law, so the co-op is likely not strictly covered. The team treated PIPEDA's principles as the standard anyway, because they are good practice and because a future funder or partner might require them. Federal privacy reform was before Parliament again in 2026; check its current status. Messaging consent falls under CASL.

Two practical points apply in both countries. First, the client, not the student team, is responsible for compliance in the long run, so the team's job is to build a system that makes compliance easy and to document what data the system holds and why. Second, if a capstone touches regulated data, such as health records, children's data or payment cards, stop and talk to the supervisor before going further; such projects need extra safeguards, and some are not suitable for a student team at all.

Worked examples

Worked problem

Minimizing a sign-up form

A draft sign-up form asks for name, email, phone, home address, date of birth and licence number. Which fields should go, and why?

Show solution
  1. Ask what each field is used for in a current feature.
  2. Address: no current use; drop it. Date of birth: no current use; drop it.
  3. Licence number: insurance requires a licence, not the number; replace with a "licence seen" checkbox for coordinators.
  4. Email and phone: one contact method is enough; let the volunteer choose.

Answer: Keep name and one contact method; drop address and birth date; replace the licence number with a coordinator checkbox.

Worked problem

A log line

An error handler logs "Failed to send reminder: " plus the whole request object. What is wrong and what should it log?

Show solution
  1. The request object contains the recipient's email or phone number and possibly a sign-in token.
  2. Logs are widely read and kept; they should not contain personal data or secrets.
  3. Log internal identifiers and the error type instead.

Answer: Log "Failed to send reminder for shift 422 to volunteer 77: provider timeout", with no contact details or tokens.

Worked problem

Deciding against a model

Give three reasons Team Relay chose a confirmation rule over a no-show prediction model.

Show solution
  1. Data: about 60 missed pickups is too few for reliable predictions, and new drivers have no history.
  2. Harm: wrong predictions could quietly reduce some volunteers' shifts in ways they cannot see or contest.
  3. Alternative: a day-before confirmation with an alert solves the same problem transparently for everyone.

Answer: Too little data, risk of hidden unfair treatment, and a simpler transparent alternative.

Common mistakes

  • MistakeCopy every column of the client's spreadsheet into the database.

    InsteadBuild a data inventory and keep only fields with a current purpose.

  • MistakePre-tick the box for text reminders.

    InsteadUse an unticked box, plain wording and an easy STOP.

  • MistakeLog whole request objects for debugging.

    InsteadLog internal IDs and error types; add a tested redaction filter as a safety net.

  • MistakeWrite a retention policy and never apply it.

    InsteadRun deletion on a schedule and log each run.

  • MistakeUse machine learning because the team knows how.

    InsteadCheck data, harms and simpler alternatives first; deciding not to build a model can be the right answer.

Summary and key terms

  • Privacy by design asks, for every feature, what data is needed, who sees it, how long it is kept and how it is protected.
  • A data inventory drives minimization: the safest data is data never collected.
  • Consent and notices must be plain; text reminders need explicit opt-in and an easy STOP.
  • Logs and backups leak personal data unless designed not to; log IDs, not contact details.
  • Retention policies must be applied automatically and audited.
  • Machine learning needs enough data, a harm assessment and a reason to beat simpler options.
  • Check even simple rules for unequal treatment; the client owns long-term compliance, the team makes it easy.
Privacy by design
Building privacy protections into a system from the start rather than adding them later.
Data minimization
Collecting and keeping only the personal data needed for a stated purpose.
Data inventory
A table of every personal data field, its purpose, access and retention.
Retention policy
Rules for how long each kind of data is kept and when it is deleted.
De-identification
Removing the link between a record and the person it describes.
Redaction
Masking personal data, such as in log lines, before it is stored.
Model card
A document describing a model's intended use, data, performance and limitations.
Dark pattern
A design that tricks or pressures users into choices they would not otherwise make.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Write a one-sentence purpose for storing a volunteer's availability.

    Show answer

    For example: "to suggest shifts that fit when the volunteer said they can drive."

  2. Name two places personal data leaks besides the main database.

    Show answer

    Logs and backups; also analytics tools, error reports and exported spreadsheets.

  3. What should happen when a volunteer clicks "Delete my account"?

    Show answer

    Contact details are removed immediately and past records are de-identified, after a simple confirmation.

  4. Give one test for a redaction filter besides "it hides phone numbers".

    Show answer

    It leaves numbers that are not phone numbers, such as years or short IDs, unchanged.

  5. Why reserve some suggestion places for new drivers?

    Show answer

    Treating unknown history as zero, or even as average, can exclude new volunteers entirely; reserved, rotated places give them a turn.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz