Exports are the point where private local records become shareable artifacts. This post explains boundary controls, review steps, and failure handling so sharing can be intentional and auditable.
Summary Boundary controls for moving from local records to shareable exports with review and rollback steps.
Exports are where the trust model changes
In a local-first app, the default promise is usually:
your data stays on your device
Exports are the moment that promise becomes conditional.
Because as soon as you create a file:
- it can be emailed
- it can be uploaded to cloud backups
- it can be forwarded
- it can be printed
- it can sit in Downloads forever
That’s not a reason to avoid exports.
It’s a reason to treat export like a deliberate boundary in both code and UX.
Pain Tracker’s export stance: explicit, local, and user-triggered
The core export utilities live here:
src/utils/pain-tracker/export.ts
The UI that invokes them (with filters) is here:
src/components/export/DataExportModal.tsx
The pattern is intentionally boring:
1) user chooses a format (CSV/JSON/PDF)
2) user optionally filters the date range / symptoms / locations
3) app generates a string (or PDF data URI)
4) app downloads it via a normal browser download
No background exporting, no scheduled exports, no “send to provider” button that quietly turns into a network feature.
The real boundary is “create a file”
Pain Tracker downloads data using a small helper:
-
downloadData(data, filename, mimeType)insrc/utils/pain-tracker/export.ts
It creates a Blob, then triggers a download by clicking an <a> element programmatically.
That’s important because it’s a clear, user-observable browser action:
- you can see the file land
- you can delete it
- you can decide where it goes
It’s a simple boundary you can explain to a tired user.
Exports are not “safe” by default (and shouldn’t pretend to be)
The CSV and JSON exports can include notes, and notes are the highest-risk field in most journaling apps.
You can see that directly in the implementation:
- CSV includes a
Notescolumn and escapes quotes - JSON is literally
JSON.stringify(entries, null, 2)
This is good honesty:
- the export is a faithful copy
- the app isn’t claiming it can “anonymize” your narrative
If you need de-identification, that’s a different feature with a different risk profile.
Treat “tracking exports” as a separate, minimal channel
Even with no backend, teams often want to answer questions like:
- do people use exports?
- which formats matter?
Pain Tracker uses two kinds of tracking around exports:
1) Local-only usage tracking
-
trackExport(type, recordCount)insrc/utils/usage-tracking.ts - it stores the last ~100 export events in localStorage
- it stores counts, not content
It also sanitizes metadata so Class A fields aren’t stored in plaintext localStorage.
2) Optional GA4 events
- export utilities call
trackDataExported(format, entryCount) - those events are gated behind env + explicit consent (covered in Part 8)
The key point is what’s not tracked:
- not the file contents
- not notes
- not the “what did you export” payload
UX: don’t make export feel like a trap
In Pain Tracker, export is presented as:
- an explicit action in an export modal
- a user-visible download
Good export UX in sensitive apps is mostly about preventing regret:
- clear format labels (CSV vs JSON vs PDF)
- clear file naming
- an obvious success state
- no surprise side effects
If you add “share” features later, treat them as new boundaries: they turn a local file into network exposure.
Next up
Part 7 covers WorkSafeBC-oriented workflows — and how to keep language careful and grounded in what the repo actually does.
Prev: Part 5 — Trauma-informed UX + accessibility as architecture
Next: Part 7 — WorkSafeBC-oriented workflows (careful language)
Support this work
- Sponsor the project (primary): https://paintracker.ca/sponsor
- Star the repo (secondary): https://github.com/CrisisCore-Systems/pain-tracker
- Read the full series from the start: (link) ## What to read next
- WorkSafeBC careful language workflow
- Storage boundary design
Top comments (0)