Somewhere between planning content, writing blog posts, and building Niobium's social media presence, I built an FHE application.
I’m 21, I study economics, and I’m a marketing intern at Niobium. My job is helping explain FHE (computing on encrypted data without decrypting it) to other people. Before this project, I had never written code, never opened a terminal, and most of what I knew about FHE came from the same materials I was supposed to be marketing. My mental image of a terminal was the black window from hacker movies. That was the entire extent of it.
The idea
The idea for the VitalVault application came from my annual physical. My bloodwork came back and I realized I understood almost none of it beyond whether a value was highlighted in red. Which bothered me, because that report was some of the most personal data I own, and my options for making sense of it were “google each number one at a time” or “upload everything to someone else’s server.” I’d been brainstorming FHE use cases for work that same week, and I kept coming back to this one: what if an app could help you understand your vitals and lab work without anyone else ever seeing them?
I decided to try to build it. This was either brave or delusional, given that at this point I could not have explained the difference between memory and storage with real confidence.
Thinking before coding
I didn’t start with code, partly because I didn’t know how to write any. I started by making ChatGPT sit through my questions. If nobody’s supposed to see my health data, who holds the secret key? Me, apparently. The user generates it and never gives it up. Then who ever sees the actual numbers? Only my own machine, which encrypts my health profile before sending it and decrypts the final report when it comes back. So what does the server get? Ciphertext. Nothing else. Writing those answers down did more than I expected: the whole structure of the app was formed by the answers. Anything plaintext happens locally, the scoring happens remotely under encryption, and the report gets formatted only after decryption. More than once I typed out a privacy claim I was proud of, ChatGPT asked me to defend it, and I watched it fall apart in front of me. Better there than in the final app, I guess.
Turning rules into math
Then I started designing the scoring itself, and this is where I found out that FHE isn't just normal programming with encryption sprinkled on top. My first rule was the most natural thing in the world: if a value is high, flag it. That is not a thing CKKS does. CKKS, the FHE encryption scheme VitalVault runs on, will add and multiply encrypted numbers all day, and that’s the entire toolbox. No “if.” No branching at all, because the computation isn’t allowed to see the values it’s operating on, which, once I sat with it, is obviously the whole point. Knowing why didn't make it any less frustrating.
For the first stretch of the design, every rule I wrote came back unusable. I’d write it the way I’d say it out loud, and encryption would refuse. The shift that finally unstuck me was changing the question. Instead of “how would a normal program do this,” I started asking “what math expresses the same idea?” So “flag high values” became: normalize each marker against a healthy reference range, and turn its distance from that range into a weighted penalty on a smooth curve. Panels combine those penalties. The overall wellness result and an estimated biological age come from said weighted panels. And anywhere I needed a function CKKS can’t compute directly, a polynomial approximation stands in for it, close enough for a wellness score.
Somewhere in there I caught myself making a different kind of mistake. I was testing the model against my own bloodwork, which happens to be unusually complete. I have thyroid labs, ferritin, vitamin D, B12, testosterone, CRP. Most people don’t. Most people have a CBC, a CMP, maybe a lipid panel and an A1C from a routine checkup, and even people who’ve had more testing done don’t have every result sitting in front of them. I was quietly designing an app that only worked for people with my exact folder of lab results. The fix became one of my favorite parts of the model: a presence mask. Skip a marker and its weight drops to zero while the remaining weights renormalize, so an incomplete profile gets scored fairly against a complete one.
Before encrypting any of this, I built the entire calculation in plaintext and kept it as my answer key, the oracle. If an encrypted result ever disagreed with it, I’d at least know which one to distrust. It was also the one part of the project where I felt fully qualified, because checking whether two numbers match is squarely within my skill set.
Letting Claude loose
Then Claude Code started writing the actual application, and the wheels came off.
Its first attempts looked completely convincing, and if I’m being generous with myself, so did my prompts.
Nothing ran.
The frustrating part wasn't that things kept failing. It was that I had no prior knowledge to understand what was actually failing. Every error looked equally impossible to me. I couldn’t tell whether Claude had made a mistake, whether I’d broken something, whether OpenFHE was complaining, whether the Niobium toolchain wanted something I hadn’t given it, or whether I’d somehow misconfigured my own laptop. Every explanation sounded equally plausible, because I didn’t know enough to rule any of them out. Each new error looked unrelated to the last one, and I spent a genuinely demoralizing stretch assuming the answer was that Claude just wasn’t good enough for this, or that I wasn’t.
Filling in the blanks
I noticed that every time Claude drifted away from the Niobium DSL, the niobium-client repository, the public repos, or the project skill I’d been given, the code got noticeably worse. It would produce generic FHE code that looked right and belonged to some other stack entirely. And every time I pushed it back toward the actual Niobium sources, it corrected itself. All those unrelated failures had a common cause. Claude wasn’t making random mistakes. It was filling in everything I hadn’t told it.
So I stopped trying to out-prompt it and started feeding it context instead. Write the encrypted computation in Niobium’s DSL. Compile against OpenFHE. Pull in niobium-client as a recursive submodule. Use CKKS with a ring dimension of 2^16 and 128-bit security parameters. No bootstrapping, so the whole thing compiles against Niobium’s toolchain and can eventually run on The Fog. I typed sentences like “use a ring dimension of 2^16” with the confidence of someone who could not define a ring dimension. And one rule I added after watching what happens without it: don’t invent parameters. Left alone, Claude fills any gap with a plausible-sounding value and keeps going. And cryptographic parameters are the last place you want “plausible-sounding.”
After a while, ChatGPT and Claude settled into completely different jobs. Claude wrote code. ChatGPT dumbed down everything I didn’t understand. A compiler error would appear, I’d stare at it for a minute hoping it would magically become obvious, then I’d paste the whole thing into ChatGPT and ask what it actually meant. Only once I understood the error could I go back to Claude with a real question instead of “why is it broken.” Claude, terminal, error, ChatGPT, back to Claude, dozens of times a day. Using one AI to understand the other felt slightly ridiculous. It also became my normal way of working.
Everything broke
My first build failed before I could even run it. The message said something about a missing library called libnbfhetch, reported by a linker. I’d never heard of either. Until that moment I’d assumed “building” a program was one step, like exporting a file. It’s at least three (compiling, linking, running), and each one can fail for its own completely separate reasons, which I got to experience personally, in order.
Git was next. VitalVault pulls in niobium-client recursively, which means repositories inside repositories inside repositories. I stared at that structure for a long time wondering why anyone would purposefully build software this way. I didn’t get an answer until later, and it arrived through a bug.
The generated C++ had started throwing warnings about doubled parentheses. I assumed I’d broken something, because at that point I assumed everything was me. Claude traced the warnings to the DSL compiler’s own output. Not my code. My instinct was to patch the file and move on. Instead, mostly out of newly acquired paranoia, I stopped and asked ChatGPT whether patching was actually the right fix. It wasn’t. That compiler lives in a pinned submodule, and editing it on my laptop would give me a build that worked on my machine and nowhere else, then broke for anyone who cloned the repository later. Which is when that repositories-inside-repositories thing finally made sense: everyone pinned to the same exact versions of everything is the reason a stranger can clone the project and have it built. I left the submodule alone and wrote the warning down instead.
At this point, I had stopped taking Claude’s confidence at face value. No single betrayal, just accumulation: convincing code that didn’t link, explanations delivered in the same certain tone whether they held up or not. Claude sounds exactly as sure of a broken build as a working one. So I started interrupting it before every big change. Explain the plan first. Let me ask about the parts I don’t follow. Then I’d go check the repository myself before believing anything had actually happened.
The stuck list
Somewhere in the middle of all this, I started keeping a list of every place I got stuck. Partly for my own sanity, partly because each entry highlighted another place where first-time builders needed more context.
Many of those gaps have since been addressed in Niobium’s FHE AI Skill, which gives AI agents like Claude instructions and context for building encrypted applications. It now asks about your FHE and programming experience and adjusts accordingly. For beginners, that means explaining concepts like batching and tradeoffs between time and accuracy, while handling most of the software and buildsetup.
When an error appears, it explains what failed and what the error means before trying to fix it. So it handles things like, compiling, linking, and initial running automatically. The skill also follows the workflow I eventually found through trial and error, starting with the privacy model and a plaintext reference before moving to encrypted code. That workflow is now enforced as a rule rather than a suggestion, leaving fewer gaps for Claude to fill in on its own.
Some of those changes came from my feedback, while others were already underway. And after three days of adding to that list, I finally stopped finding new things to put on it (for now).
Proving it worked
By the time everything finally compiled, I just couldn't believe it actually worked. I'd spent three days watching convincing-looking code fail, and "it compiled" didn't mean much anymore. The only proof I was willing to accept was the answer key. So I ran the whole pipeline end to end, key generation through decryption, and put the two results side by side.
I needed a complete set of lab values to test it, so I did what everyone does: I Googled one. About ten seconds later, I found a published demo dataset from a 47-year-old female. Problem solved.
The encrypted computation estimated a biological age of 46.744. The plaintext oracle returned 46.74. About 0.004 apart, with the largest panel-level difference at 0.075, exactly where the polynomial approximations should land.
Seeing those numbers line up changed how I thought about FHE. The encrypted version had never seen a single real value. It had only ever operated on ciphertext, yet it arrived at essentially the same answer as the plaintext version that could see everything.
After that, of course, I ran my own labs. VitalVault estimated my biological age at 25. Apparently I have a few things to work on…
For about thirty seconds I let myself think I was done. Then I remembered the fresh clone.
I had one test left, the one I’d been dreading: rebuilding the whole project from a fresh clone (all 21 nested submodules) to prove it wasn’t quietly depending on leftover files from my machine.
It's worth mentioning that every build, every encrypted run, and every test happened on an 8 GB MacBook Air, which I now understand is not the machine anyone would choose for heavy encrypted computation. For three days the fans sounded like they were preparing for takeoff, and the laptop got so hot that I genuinely thought it was about to explode. Every successful build felt like a win for both the software and the laptop.
Eventually, my laptop gave one last dramatic performance. It built. It ran.
A month ago, I thought I'd spend the summer planning content, writing blog posts, and building Niobium's social media presence. While I still did all of those things, I also ended up building an FHE application that anyone can clone, build, and run. I didn't see that one coming.
VitalVault is open source at github.com/leila-db/vitalvault-demo. It’s an educational demo, not a production reference implementation.