DEV Community

CrisisCore-Systems
CrisisCore-Systems

Posted on

Exports are a security boundary: the moment local-first becomes shareable

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) in src/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 Notes column 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) in src/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

Top comments (0)