Building SHALA: An AI Mental Health Companion with Zero-Knowledge Encryption
Tech How do you build an AI-powered mental health app that users can truly trust? The answer isn't just about a smart AI model; it's about Absolute Privacy and Data Security. Through SHALA (Supportive Help Agent and Li
Tech
How do you build an AI-powered mental health app that users can truly trust? The answer isn't just about a smart AI model; it's about Absolute Privacy and Data Security. Through SHALA (Supportive Help Agent and Lifeline Assistant), I implemented a Zero-Knowledge Encryption architecture, a Multi-Provider AI Gateway with 11 providers, and layered defenses to protect users' most sensitive data. This article breaks down the threat model, technical decisions, and lessons from real incidents.
1. Zero-Knowledge Encryption Architecture (AES-GCM + PBKDF2)
One of SHALA's core features is the Safe Space Diary. Users can lock their journals with a PIN. The key never touches the server.
- KDF: The user's PIN is derived using PBKDF2 (100,000 iterations, SHA-256) directly in the browser via the WebCrypto API.
- Encryption: Journal content is encrypted using AES-256-GCM (Galois/Counter Mode) with a unique Initialization Vector (IV) for every entry.
- Zero-Knowledge: The server (Firestore) only receives ciphertext. Even database administrators cannot read the user's journal.
Conceptually, the flow is simple:
-
key = PBKDF2(PIN, salt, 100000)โ derive the key from the PIN -
ciphertext, iv = AES-GCM-encrypt(plaintext, key)โ encrypt content -
plaintext = AES-GCM-decrypt(ciphertext, key, iv)โ decrypt when opened - What reaches the server:
{ciphertext, iv, salt}โ never thePIN, never thekey, never theplaintext
The practical consequence: if the user forgets their PIN, the data is unrecoverable. This is a deliberate trade-off โ absolute privacy over recovery convenience.
2. WebCrypto Rationale & Web Worker
Why use the native WebCrypto API instead of third-party libraries (like CryptoJS)?
- Native & Fast: Runs at the browser engine level, utilizing hardware instructions (AES-NI).
- Secure: No risk of supply-chain attacks from npm libraries.
- Non-Blocking: The heavy encryption/decryption process (100,000 PBKDF2 iterations) executes inside a Web Worker, so the main thread (UI) never freezes or lags while the user types.
A Web Worker is not just an optimization โ it is a functional requirement. 100,000 PBKDF2 iterations on the main thread will freeze the UI for 300ฮรรด800ms on mid-range mobile devices. During that freeze, the user may hit back or assume the app is hanging. Moving it to a Worker keeps the UI responsive; the password field can show a spinner while derivation runs in the background.
3. Threat Model: What Is Protected, What Is Not
A security system without an explicit threat model tends to overclaim. SHALA distinguishes honestly:
What is protected:
- Journal contents from database administrators, cloud providers, and Firestore breaches
- The user's PIN โ never sent to the server, never stored
- AI provider API keys โ only exist on the backend proxy, never in the client JS bundle
What is not protected (transparent):
- Metadata: timestamps, entry size, write frequency, and login status remain visible to Firestore
- AI prompts: when a user requests AI module help, prompt content is sent plain-text to the backend proxy and forwarded to the AI provider. The gateway does not store prompts, but third-party providers may have their own retention policies
- Registration: user email for Firebase Authentication is stored plain-text (by design โ Firebase Auth needs it for login)
Conclusion: encryption protects the content of the journal, not its existence or usage patterns. Users who need metadata privacy must understand this boundary.
4. Multi-Provider AI Gateway & Circuit Breaker
SHALA does not rely on a single AI provider. We built a backend Gateway (Express.js) that routes requests through a fallback chain with 11 providers: Gemini ฮรฅร Groq ฮรฅร GitHub Models ฮรฅร Mistral ฮรฅร Z.ai ฮรฅร DeepSeek ฮรฅร Cerebras ฮรฅร SambaNova ฮรฅร Cloudflare ฮรฅร OpenRouter ฮรฅร Pollinations.
If the primary provider times out or hits a rate limit (HTTP 429), the system automatically switches to the next provider in milliseconds.
The circuit breaker concept here:
- Each provider has a state:
closed(normal),open(skip, because it already failed N times consecutively),half-open(try once for recovery) - When the failure threshold is reached, the provider is marked
openfor a window โ not every request tries a provider that is clearly down - Requests move to the next provider in the fallback chain
- If all providers are
open, the system shows a friendly error โ not a stack trace
Why 11 providers, not 3? Because during peak hours, popular providers (Gemini, Groq) often hit rate limits. A longer chain increases availability at the cost of higher operational overhead.
5. Crisis Detection
The Gateway is also equipped with Crisis Detection โ high-risk keyword detection that immediately surfaces emergency helpline numbers when necessary. This is not an optional feature; it is a mandatory safety layer for a mental health app.
The flow:
- Every user prompt is scanned against a keyword list (phrases like "want to end it", "no hope left", etc.) โ at minimum bilingual (ID + EN)
- On match, the response is augmented with a grounding prompt + local hotline number
- A dedicated rate limit prevents accidental repeated triggering
- False positives can happen โ if a user writes "I want to end this movie", the system should not panic. Solution: keyword matching + sentence context + soft warning (not a hard block)
This is an area that must ethically fail-safe: better a false positive (excess warning) than a false negative (missed detection).
6. Firestore Security Rules
Firestore is a database accessed directly from the client via SDK. Without strict rules, anyone with a Firebase API key (which exists in the JS bundle โ by design) can read collections they should not.
Our principles:
-
Default deny: root catch-all
allow read, write: if false -
Owner-based: all user subcollections (
/users/{uid}/diaryEntries/{id}) are accessible only by users with the sameuid -
Admin-based: system collections (
system_errors,admin_logs,admin_metrics) can be read only by admins via custom claim
A common mistake: allowing isSignedIn() alone on collections like replies (Paper Plane). That means any signed-in user can write to anyone's collection. We had this bug once โ and found it during audit. Now every write on replies requires request.resource.data.userId == request.auth.uid.
7. Sentry PII Redaction (GDPR & UU PDP)
Observability without scrubbing = privacy violation. Sentry receives error stacks, breadcrumbs, and session replays โ all of which can contain PII.
SHALA does automatic redaction before data leaves the application, both client-side (beforeSend & beforeBreadcrumb) and server-side (beforeSend in Sentry Node):
-
Email: regex ฮรฅร
[EMAIL_REDACTED] -
Phone: regex ฮรฅร
[PHONE_REDACTED] -
API Key / Bearer Token: regex ฮรฅร
[API_KEY_REDACTED] -
Sensitive fields (
password,token,secret,creditCard): stripped from extra context
Session Replay is also masked for input fields โ PII must not appear in replay video.
Compliance: GDPR (EU) and UU PDP (Indonesia) require data minimization and purpose limitation. Scrubbing at the source is safer than at the destination โ if data has been sent, it's too late.
8. i18n: 13 Languages, 2 RTL
SHALA supports 13 languages: English, Indonesian, Spanish, French, German, Japanese, Korean, Chinese (Simplified), Arabic, Urdu, Hindi, Portuguese, Russian.
Two of them โ Arabic and Urdu โ are RTL (Right-to-Left), requiring full layout mirroring: sidebar moves to the right, arrow icons flip, padding swaps.
An honest reality: not all languages have 100% coverage. English and Indonesian = 100%. European languages = 70ฮรรด78%. East Asian languages and Urdu = 11ฮรรด12% (still in translation). This is a deliberate choice โ prioritize clinical translation quality (CBT terms, grounding) in primary languages, not speed of completing every language.
9. Incident & Challenge: Firestore Multi-Tab Crash
One of the most challenging incidents was when Firestore crashed with the message e.Vu.get is not a function. It turned out that the multi-tab sync feature (persistentMultipleTabManager()) on Firestore failed when the app was run inside a sandboxed iframe (like in AI Studio preview).
Solution:
- Switched to
persistentLocalCache({})(single-tab mode) โ dropped multi-tab sync, but eliminated the crash - Vite configuration (
optimizeDeps.exclude) for the entirefirebasepackage โ prevented esbuild from breaking Firebase's ESM iterability
Lesson: default Firestore multi-tab is very useful for normal apps, but incompatible with sandboxed environments that have IndexedDB constraints. Always test in the target deployment environment, not just the dev browser.
10. Lessons Learned
Three key lessons from building SHALA:
A Web Worker is not an optimization, it's a functional requirement. 100k-iteration encryption on the main thread = UI freeze. Move it to a Worker or users will think the app crashed.
The threat model must be explicit and honest. Better to write "metadata is not protected" in the docs than to claim "100% private" and lose trust when users understand the limits.
Firestore rules that look "secure enough" are often security holes. Default deny + owner check + periodic audit. The Paper Plane permission bug was found during audit, not at runtime.
SHALA is a project built with React 19, TypeScript, Express, Firebase Authentication & Firestore, and the WebCrypto API. The 11-provider AI gateway architecture with fallback chain and crisis detection is core to the app's resilience.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.