Erlang, Vue, and Client‑Side Encryption by Design
A close friend of mine works as an AIDS counsellor.
One day she told me something that stayed with me:
“My clients want companionship. They want marriage. They want normalcy.
But they’re terrified of being exposed.”
She had seen it repeatedly — people who were HIV‑positive, stable on treatment, undetectable… but isolated. Dating apps didn’t feel safe. Disclosure was complicated. Screenshots could ruin lives. A database leak would be catastrophic.
Then she asked me:
“Can you build something for them?
But it has to be completely private.”
Not “secure enough.”
Not “we encrypt passwords.”
Completely private.
That’s where this journey started.
Why Traditional Dating Apps Fail This Community
Most dating platforms assume the server is a trusted party:
- The server stores your profile
- The server reads your profile
- The server decides who sees your profile
- The server can be breached, subpoenaed, or misused
For HIV‑positive communities, this model is dangerous. Disclosure can affect employment, housing, family relationships, and mental health. Privacy isn’t a feature — it’s psychological safety.
So I flipped the premise:
What if the server never sees plaintext user data at all?
Not at rest. Not in logs. Not in admin dashboards. Not even temporarily.
That led to a zero‑knowledge architecture.
The Core Principle
The Server Must Not Be Able to Read Profiles
Instead of being a traditional CRUD backend, the server becomes:
- A cryptographic relay
- A coordination layer
- A match facilitator
- Not a data owner
Everything meaningful happens on the client.
The Stack
The platform combines:
- Erlang + Yaws for backend reliability
- Vue 3 + Naive UI for the frontend
- Google OAuth for authentication
- TweetNaCl for client‑side cryptography
- Deterministic key derivation using handle + PIN
- Hybrid symmetric + asymmetric encryption for profiles and matching
This stack supports a fully zero‑knowledge flow without sacrificing usability.
Step 1: OAuth for Authentication, Everything Else Encrypted
Google OAuth is used purely for login and identity verification.
Once authenticated:
- The email is encrypted client‑side
- A hash of the encrypted email is stored for lookup
- Only minimal metadata is visible to the server:
- OAuth provider
- User role
- Membership tier
All profile data is encrypted before it ever leaves the browser.
If the database leaks, it’s unreadable.
Step 2: Client‑Side Encryption of Profiles
When a user creates a profile:
- A symmetric AES key is generated in the browser
- The entire profile is encrypted client‑side
- Only encrypted blobs and wrapped keys are sent to the server
The database stores:
- Encrypted profile vault
- Encrypted “card preview”
- Wrapped encryption keys
- Public keys
- Search‑safe hashed fields
The server never sees readable health, lifestyle, or personal information.
Step 3: Deterministic Key Derivation Using Handle + PIN
To reduce friction, we avoided traditional passwords.
Instead:
- Users choose a handle and a PIN
- These values become salt inputs for key derivation
- Keys are derived entirely in the browser
- The PIN is never stored
This means the server cannot brute‑force vaults, even if it wanted to.
What this achieves:
A login flow that feels simple but produces strong cryptographic guarantees.
Step 4: Public / Private Keys for Matching
Each user generates a public/private key pair in the browser.
- The public key is sent to the server
- The private key is encrypted client‑side using the PIN
Matching uses a controlled key‑wrapping flow:
- User vault keys are wrapped with the user’s public key
- The server temporarily unwraps with its private key
- The server re‑wraps with the requesting user’s public key
- The frontend unwraps locally
A simple metaphor:
It’s like passing a sealed envelope through a courier who can’t read it — only re‑seal it for the next recipient.
At no point does the server retain readable profile data.
Step 5: Erlang + Yaws for Reliability
Why Erlang?
- Massive concurrency
- Fault tolerance
- Lightweight processes
- Predictable performance under load
Yaws serves dynamic HTML with embedded markers (data-app-id, session IDs, etc.) which Vue hydrates on the frontend.
This gives us a modern UI with a backend that is resilient, minimal, and secure.
Step 6: AI Matching Without Leaking Profiles
Compatibility is computed using client‑generated embeddings:
- Each browser computes embeddings locally
- Embeddings are normalized and binarized
- Only non‑reversible binary hashes are sent to the server
The server compares hashes — it never sees raw interests, lifestyle traits, or personal details.
What this achieves:
AI‑powered matching without exposing sensitive information.
Threat Model: What Happens If Everything Goes Wrong?
This architecture protects users even under worst‑case scenarios:
- Database leak: attackers get only encrypted blobs
- Rogue admin: cannot read profiles
- Server compromise: no plaintext data to steal
- Network interception: TLS + client‑side encryption
- Legal pressure: server literally cannot decrypt anything
The system is designed so that no one — including me — can read user profiles.
Lessons Learned
Most apps are built around:
“How do we store and use data?”
This app is built around:
“How do we avoid ever possessing data?”
That shift changes everything:
- UX
- Cryptography
- Backend architecture
- Matching logic
- Threat modeling
- Error handling
- Recovery flows
It forces you to design with humility — assuming the server is a liability, not a guardian.
Next Steps
Future posts will dive into:
- Key‑wrapping flows in practice
- Account recovery without exposing secrets
- Erlang concurrency for cryptographic workloads
- Zero‑knowledge search and abuse prevention
- UX patterns for client‑side encryption
This project started with a counselor who wanted her clients to feel safe finding love.
It evolved into one of the most technically challenging — and meaningful — systems I’ve ever built.
For sensitive communities, privacy‑first design isn’t optional.
It’s essential.
Top comments (0)