If you’ve built software in healthcare, you know the usual arrangement. The firm signs a business associate agreement, the developers get PHI access, and everyone agrees to be careful. Careful means scrubbing logs, keeping patient data out of screenshots and tickets, and trusting that nobody pastes the wrong thing into the wrong window. The health assessment app we built for a large health system is write-only instead.
It walks a patient through a long clinical questionnaire and scores their risk for a chronic condition, and it can push those results into their medical record. I have never seen a single patient’s answers.
That isn’t discipline on my part. The app is write-only. It writes data out and cannot read any of it back, not other people’s records and not even its own. The database rules enforce it: write if authenticated, read denied. There is no path through the application that would show me a survey.
Debugging without PHI access
When I need to debug a data problem, here is what I have to work with: a document ID and a blob of ciphertext. I can’t tell you whose survey it belongs to. I can’t tell you what they answered. So I cross-reference an anonymous user ID against timestamps in the logs. “10:29, okay, that was me testing the portal login.” It works. It’s slower than just looking at the data.
It’s also the point. Log an email address in plain text somewhere, which is an easy mistake in an app that tracks user events, pair it with the document ID sitting next to it in the logs, and now you know Jordan screened positive for a condition they’d rather not have attached to their name. A small logging mistake can undo a lot of encryption, and it’s the kind of mistake that gets made by someone competent on a Tuesday.
I inherited this app after most of its architecture was already in place, and for a while that rigidity mostly showed up as friction. Then the client’s security team asked us for a data flow diagram, and I got a much better appreciation for what the earlier decisions had bought us.
The app is a write-only pipe
The identifiers attached to a survey are deliberately thin. The primary one is an anonymous GUID generated when the user starts. We don’t ask for a name or a date of birth. We do ask for an email address and a ZIP code near the end, and the email is optional and only used for follow-up feedback about the app.
From there the data moves through two databases. The app writes encrypted survey data into an intermediary collection, which is the only place our permissions and security rules let us write. Cloud functions and a small service running on the client’s infrastructure watch that collection. When new data lands, their code decrypts it, re-encrypts it with a key that exists only on their side, and writes it to its permanent home.
The key our app uses opens the staging area and nothing else. The archive is encrypted with a key we have never held and have no reason to hold. We never touch the second database, and we never read back from the first.
The patient portal handoff
The EHR integration works the same way, and this part is kind of slick. Before the survey starts, a user can log in through the health system’s patient portal. Their portal sends the browser to an endpoint on the client’s own backend with a short-lived, single-use authorization code attached. Their backend exchanges that code, finds the actual patient in the EHR, and pushes the survey results into that patient’s record. Our app never transmits the code and never learns who the patient is. If a user skips the portal login entirely, their data sits in the database and does nothing at all.
It only ever pushes. The app is dumb, and I mean that as a compliment. It could almost be a Google Sheet. It has one responsibility, which is recording survey answers and handing them off, and it doesn’t know or care what happens after that.
What write-only cost us
The worst of it was the EHR integration. Results weren’t landing in patients’ records and I couldn’t see why. The failure was somewhere in the cloud functions, the code that picks up a finished survey, decrypts it, and hands it to the EHR. I couldn’t read the document that failed, and the functions aren’t ours to deploy.
The other cost was a feature. The survey is long, and users were dropping out halfway through. Before, if you quit, you started over from the beginning. Normally resuming is simple: you read the user’s previous answers back from the database and pick up where they left off. We couldn’t do that, because the app doesn’t read from the database. That is the whole design.
We floated adding user accounts so we could fetch a saved survey. Instead we store answers locally on the device as the user moves through sections. A person’s own answers, sitting on their own phone, deletable by them at any time, isn’t a disclosure. We confirmed that reading with the client before we built it.
I want to be careful not to dress this up as a better outcome for the user, because it isn’t. A server-side resume would have done the same job and survived more: a new phone, a reinstall, a cleared cache. Ours doesn’t. Delete the app and the answers go with it, even though the record is still sitting in the database, where nobody can reach it, including the person who filled it in. What we got was the drop-off fix without taking on accounts and identity. What we gave up is durability. The client made the call knowing it.
The payoff nobody designed for
When the security team asked for that data flow diagram, drawing it was easy, and that was the point. We could put a line on it and say that past this point we no longer control the data and can’t get it back. Once we’ve pushed to the intermediary collection, Atomic Object is not the exposure. There’s a real difference between telling a health system “we checked everything and we don’t think anything is wrong” and telling them “the system is write-only.” The second is the one they can act on, and you can only offer it if you designed for it up front.
One backend, two frontends
The other payoff arrived at the same time. We’re standing up a web version of the app, and it reuses the same database, the same cloud functions, and the same EHR integration. Not one line of data handling changed.
That isn’t because React Native Web is magic. Plenty had to change, and none of it was free. But all of it was frontend work. How data is encrypted, where it goes, who can read it, and how it reaches the medical record never came up. Those were never ours to move.
When your frontend owns nothing, you can replace it cheaply, and you also can’t fix anything that isn’t in it. So the question worth asking early is what the app needs to know at all. It’s easy for that list to grow. Somebody reasonably suggests being able to log in and look at results, and now you’re managing accounts, sessions, and a much larger PHI surface. On this project the list stayed short. Three years later, it’s the decision I’d least want to undo.