DEV Community

Cover image for I built a CBT mirror that runs 100% on-device — then gave it away
soundmandt
soundmandt

Posted on AI-assisted

I built a CBT mirror that runs 100% on-device — then gave it away

There's a version of this story where a guy has a hard year, builds an app about it, and puts it on a store. This isn't that story, because the app store was never going to let this exist the way it needed to.

I'm a sound engineer. Thirty-five years behind mixing boards. I am not a therapist, and The Vault is not therapy. It's a mirror: you type what's actually going on, and it reflects it back at you the way cognitive behavioral therapy teaches — spot the cognitive distortion, name it plainly, get offered a reframe that isn't a greeting-card slogan. No coddling. The mirror doesn't hug you. It shows you the shape of your own thinking and lets you decide what to do with it.

It was built for the people who will never get near actual mental healthcare. Cost, waitlists, geography, pride — pick your wall. If that's your situation, a sounding board that works at 2 a.m. and forgets everything you said is not a compromise. For a lot of people it's the only version that was ever going to happen.

Nothing stored is not a feature. It's the product.

Every design fight on this project ended at the same wall: the moment this thing has a server, an account, a database, analytics — it stops being safe to tell the truth to. So it has none of those.

The Vault is one HTML file plus a lib folder. Open it in a browser and a small language model runs on your device:

WebGPU path: WebLLM running Llama 3.2 1B
CPU fallback: Transformers.js running Qwen 2.5 0.5B

First open downloads the weights (roughly 800 MB on the GPU path, 400 MB on CPU) and caches them. After that it works offline, including on phones — it picked the GPU engine and ran fine on my iPhone, which still slightly blows my mind, because nothing about this went through Apple's ecosystem at all. It's a web page.

Conversation memory is five exchanges, held in RAM. There is a PURGE button, same size as SEND, one press, no confirmation dialog. Nothing is written to localStorage, ever. The TAKE FIVE button — slow breathing and a body scan, paced by voice — works before the model even loads, because the moment you need it is not the moment to watch a progress bar.

The crisis net is narrow on purpose

This is the decision people argue with, so I'll make the argument myself.

Most mental-health software treats any mention of self-harm as a five-alarm fire: interrupt everything, replace the conversation with a hotline form letter, lock the user out of the tool they came to. I understand why lawyers love that. I also know what it feels like from the other side of the screen: you finally said the true thing out loud, and the machine responded by hanging up on you — politely, with a phone number.

The Vault splits the world in two:

Imminent or active danger — a plan for tonight, pills already taken, bleeding that won't stop, voices issuing commands — stops everything. A human sentence, not a form letter ("I'm a mirror, not a lifeline…"), plus tappable real numbers: 988, 911, Poison Control, 211, the Crisis Text Line.
Ideation — "I want to die," the long dark middle most people actually live in — gets listened to. Serious mode, the mirror keeps working, and the same numbers sit in a box on screen, offered rather than imposed.

The matcher that makes this call is clause-based and negation-aware — it reads "I'm not going to hurt myself" differently from "I'm going to hurt myself tonight," because those are different sentences and they deserve different responses. PRs that widen the net get declined, and the README says why. Somebody is in a real crisis whether they have this app or not. Better to have it.

Why GPL, and why now

I could have kept it on my hard drive, where it was doing exactly one person any good. It's GPL v3 now — derivatives must stay open — at https://github.com/soundmandt-dot/the-vault, live at https://soundmandt-dot.github.io/the-vault/

Here's the part I mean most: I can't finish this. There are three issues open in the repo that are really doors:

A Spanish translation. Not a translation of the words — the safety matchers must be rebuilt in Spanish, negation and all, and the crisis numbers are jurisdiction-specific. A translator in Buenos Aires or Madrid owns things I can't: the language, the local numbers, and the battered low-end phone it has to run on.
A service worker, so the page itself — not just the model — survives fully offline.
Matcher test batteries. More scenarios trying to break the line between ideation and emergency, so the line holds.

I build sound for a living. I made this across some late nights because it needed to exist and nobody was selling it without a server attached. It's yours now. If you can carry it somewhere I can't — your language, your country, your $80 phone — that's not a fork. That's the point.

Top comments (4)

Collapse
 
ahmedznouda profile image
Ahmed ZNOUDA •

I work on a mental health screening app, and the issue I'd most like to help with is the matcher test battery, since everything hangs on that line. Cases that tend to break clause-based matchers:

  • Past tense, nothing in the future: "I took the whole bottle an hour ago".
  • Someone else in danger: "my sister just took pills".
  • Negation that doesn't reach the clause that matters: "I'm not okay and I took them" vs "I didn't take them".
  • Reported speech: "my therapist asked if I'd hurt myself, I said no".
  • Typed at 2 a.m.: no punctuation, typos ("pils", "od"), slang ("kms", "unalive").
  • Warning signs that never use the words: "gave my dog to the neighbour and wrote everyone a letter".

I'd write each case as a pair, one that should cross the line and a near twin that shouldn't, so both a widening PR and a narrowing PR show up as failures.

For the Spanish work, I'd keep the numbers out of the matcher entirely: a table keyed by country, so someone in Madrid sees 024 instead of 988.

Collapse
 
soundmandtdot profile image
soundmandt •

Thank you — this is the comment I was hoping somebody would write. The past-tense case is the one that sits with me: "I took the whole bottle an hour ago" is already an emergency, and a matcher built around plans for tonight could walk right past it. And "someone else in danger" is a real gap. The pair-per-case method is the right shape: a widening PR and a narrowing PR both have to fail loudly before I merge anything. Issue #3 is yours if you want it. And the country-keyed numbers table for the Spanish work is cleaner than what I had in my head

Collapse
 
ahmedznouda profile image
Ahmed ZNOUDA •

Happy to. I'll start with the pairs from my comment plus the ones in the issue (curly apostrophes, non-breaking hyphens, denial shapes, code-switching), as test cases only, no matcher changes, so you stay the one deciding what the net should catch. I can add a few French-English code-switching pairs too. Expect a first PR within a week or so.

Thread Thread
 
soundmandtdot profile image
soundmandt •

Ahmed, thank you. I really appreciate you taking this on.

I'm a real person behind this, not a company — and this project matters to me because it's for people who will never get healthcare.

Test cases only is exactly right. I want to stay the one deciding what the net catches. The French-English pairs are a great addition too.

Looking forward to the PR,
Danny