Skip to main content

Privacy & PII

Helm holds personally identifiable information for every customer of every shop. The principle: treat it like the legal liability it is. Know where it is. Know where it goes. Make removing it easy.

Drafted from planning · v0.1

What counts as PII

In every shop's D1:

  • customers.first_name, last_name, email, phone, address_line_1/2, postal_code, dob, account_number
  • customer_notes.body (free text — may contain PII)
  • customer_marketing_prefs.email, phone (consent records hold the contact methods)
  • customer_bikes.serial_number (treated as PII; bikes are personal property)
  • service_ticket_messages.body (SMS body — likely contains addressing of the customer)
  • staff.first_name, last_name, email, phone, pin_hash, pin_salt (staff is a different actor but their identifiers are also PII)
  • ai_messages.content_json (may contain quoted customer data)
  • R2 photos for service tickets (faces of customers, plates, addresses on receipts)

What is not PII:

  • Internal IDs (customers.id, etc.)
  • Aggregate counters
  • Transaction line items themselves (when the customer_id is stripped)
  • Tax IDs of the shop (the shop is the business; this is operating data)

Where PII goes

Outbound flows that touch PII:

  • Stripe API — customer name and email on receipts; payment-method tokens.
  • Twilio API — phone number, SMS body, customer first name.
  • Anthropic Claude API — grounding payloads can include PII unless the customer has opted out. Defaults are conservative; see AI integration.
  • Email provider (Resend/Postmark) — recipient address + body.
  • R2 — receipt PDFs (customer name + email + transaction history), photos.
  • Audit logbefore_json / after_json snapshots of mutations on PII-bearing tables.

Outbound flows that don't touch PII:

  • GBP API — review IDs and contents; the customer identity is GBP's, not ours.
  • Vendor APIs (QBP, Shimano) — SKUs and quantities; no customer info.

What "PII handling" means concretely

A short list of obligations Helm honors:

Don't log PII to console or external logging services. Worker logs at INFO level should never contain a customer's email, phone, or full name. Use IDs and labels. If a debug log needs the value, gate it behind an explicit dev-environment check.

Encrypt at rest, encrypt in transit. D1 + R2 are encrypted at rest by Cloudflare. All outbound API calls use HTTPS. No exceptions.

Honor the AI opt-out toggle. The customer profile has ai_optout. When set, the Worker's AI-grounding step skips that customer's identifying fields and logs the conversation with customer_id = NULL. See data ownership.

Provide an erasure path that respects the audit constraint. The Settings → Data → Erasure Requests flow scrubs PII fields from the row (set to NULL or "Deleted Customer") while keeping the row's ID so the audit chain and financial history remain valid. The scrub action is itself an audit-logged event.

Scrub PII from R2 on erasure. Receipt PDFs that name the erased customer are regenerated as "Customer #4521 (erased)" or deleted (depending on retention policy). Service-ticket photos showing the customer are deleted.

Cap retention. Receipts: 90 days in R2 (regeneratable from D1 indefinitely; raw PDF deleted). Photos: 1 year. Audit archives: 7 years (regulatory floor for financial records).

What we don't do

  • No third-party analytics that sees PII. No Mixpanel, no GA, no Heap, no LogRocket. The operator app is internal; we use Workers Analytics for traffic counts only.
  • No customer-facing tracking pixels. The public shop site (separate Worker) may use shop-owned analytics; the operator app does not.
  • No PII in error messages shown to other users. "Customer not found" — not "Customer 'Jane Doe' (jane@example.com) not found." Internal logs may include the ID; user-facing error UI uses generic copy.
  • No PII in URLs. Customer profile URLs use IDs (/customers/4521), not slugs derived from names. Search query strings are POSTed when they may contain PII rather than GET (so they don't end up in server access logs).
  • No second-party data sharing. A vendor that wants "your customers' email addresses to send promotions" gets a hard no.

Per-shop differences

The shop is the data controller; Helm is the processor. The shop's privacy policy is the customer-facing document; Helm provides the infrastructure to honor it. If a shop's policy says "we delete inactive customers after 5 years," Helm provides the scheduled job; the shop sets the policy.

Helm provides reasonable defaults (the values in shop_config.privacy_* columns) and exposes them in Settings → Privacy. Shops can override per their jurisdiction's requirements.

Multi-jurisdictional notes

Most current Helm shops are in British Columbia (PIPA + federal PIPEDA apply). When a shop in Quebec (PQ) signs on, the additional Loi 25 requirements apply — primarily the right to know what's automated decision-making (the AI Support bubble notes apply here). When a shop in California signs on, CCPA-style data sale prohibitions apply (already aligned with Helm's defaults).

The schema does not branch by jurisdiction. The behaviors that branch (retention, consent UI copy, AI disclosure) read from shop_config flags set per shop.

See also