- Zero-knowledge encryption for ALL sensitive data + metadata - Blood pressure example: value + type + unit ALL encrypted - 9 collections: users, families, profiles, health_data, lab_results, medications, appointments, shares, refresh_tokens - Client-side encryption (AES-256-GCM, PBKDF2) - Server NEVER decrypts data - Privacy-preserving queries (plaintext fields: userId, profileId, familyId, date, tags) - Tagging system for encrypted data search - Date range queries (plaintext dates) Key principle: - Both value AND metadata encrypted (e.g., "blood_pressure" + "120/80") - No plaintext metadata leaks - Server stores ONLY encrypted data Updated tech stack decisions with MongoDB schema All major research complete (Rust, Mobile, Web, State, Auth, Database) Next: Backend development (Axum + MongoDB)
28 KiB
MongoDB Schema Design for Normogen
Date: 2026-02-14 Focus: Zero-knowledge encryption for all sensitive data AND metadata Database: MongoDB 6.0+
Table of Contents
- Zero-Knowledge Encryption Requirements
- Database Architecture Overview
- Collection Schemas
- Encryption Strategy
- Indexing Strategy
- Privacy-Preserving Queries
- Data Migration
- Performance Considerations
Zero-Knowledge Encryption Requirements
Core Principle
ALL sensitive data AND metadata must be encrypted client-side before reaching MongoDB.
What Must Be Encrypted
Health Data (Value + Metadata)
// Blood pressure reading - BOTH value AND metadata encrypted
{
value: "10", // Encrypted ❌
metadata: {
type: "blood_pressure", // Encrypted ❌
unit: "mmHg" // Encrypted ❌
}
}
// After encryption (stored in MongoDB)
{
value: {
encrypted: true,
data: "a1b2c3d4...",
iv: "e5f6g7h8...",
authTag: "i9j0k1l2..."
},
metadata: {
encrypted: true,
data: "m3n4o5p6...",
iv: "q7r8s9t0...",
authTag: "u1v2w3x4..."
}
}
What Can Be Plaintext
// ONLY non-sensitive, non-identifying fields
{
userId: "user-123", // Plaintext (for queries) ✅
familyId: "family-456", // Plaintext (for family queries) ✅
profileId: "profile-789", // Plaintext (for profile queries) ✅
createdAt: ISODate("2026-02-14"), // Plaintext (for sorting) ✅
updatedAt: ISODate("2026-02-14"), // Plaintext (for sorting) ✅
// ALL health data encrypted ❌
healthData: [
{
encrypted: true,
data: "...",
iv: "...",
authTag: "..."
}
]
}
Why Metadata Must Be Encrypted
Problem: If metadata is plaintext, attackers can infer sensitive information.
Example:
// BAD: Metadata plaintext (leaks information)
{
userId: "user-123",
healthData: [
{
type: "hiv_test", // Reveals HIV status
result: "positive", // Reveals HIV status
date: "2026-02-14", // Reveals when tested
doctor: "Dr. Smith", // Reveals healthcare provider
}
]
}
// GOOD: Metadata encrypted (privacy-preserving)
{
userId: "user-123",
healthData: [
{
encrypted: true,
data: "a1b2c3d4...", // Encrypted: type + result + date + doctor
iv: "e5f6g7h8...",
authTag: "i9j0k1l2..."
}
]
}
Database Architecture Overview
Database Structure
normogen (database)
├── users (collection)
├── families (collection)
├── profiles (collection)
├── health_data (collection)
├── lab_results (collection)
├── medications (collection)
├── appointments (collection)
├── shares (collection)
└── refresh_tokens (collection)
Data Flow
Client (React Native / React)
├── User enters data
├── Client encrypts data (AES-256-GCM, PBKDF2)
├── Client sends encrypted data to server
│
Server (Axum / Rust)
├── Server receives encrypted data
├── Server NEVER decrypts data
├── Server stores encrypted data in MongoDB
│
MongoDB
├── Stores ONLY encrypted data
├── No plaintext sensitive data
└── Zero-knowledge architecture maintained
Collection Schemas
1. Users Collection
Purpose: User authentication and account data
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for authentication)
userId: { type: String, unique: true, required: true },
email: { type: String, index: true, required: true }, // Plaintext for login
passwordHash: { type: String, required: true }, // Plaintext (bcrypt hash)
tokenVersion: { type: Number, default: 1 }, // Plaintext (for JWT revocation)
// Encrypted fields (zero-knowledge)
encryptedRecoveryPhrase: {
encrypted: true,
data: String, // Encrypted recovery phrase
iv: String,
authTag: String
},
// Family relationships
familyId: { type: String, index: true }, // Plaintext (for family queries)
familyRole: { type: String }, // Plaintext (parent, child, elderly)
permissions: [String], // Plaintext (for JWT permissions)
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
lastLoginAt: Date
}
Encryption Notes:
- ✅ Plaintext:
userId,email,passwordHash,tokenVersion,familyId,familyRole,permissions - ❌ Encrypted:
encryptedRecoveryPhrase
Indexes:
// Indexes for performance
db.users.createIndex({ userId: 1 }, { unique: true });
db.users.createIndex({ email: 1 }, { unique: true });
db.users.createIndex({ familyId: 1 });
db.users.createIndex({ createdAt: -1 }); // For sorting
2. Families Collection
Purpose: Family structure and relationships
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
familyId: { type: String, unique: true, required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
// Encrypted family name (privacy-preserving)
familyName: {
encrypted: true,
data: String, // Encrypted family name
iv: String,
authTag: String
},
// Encrypted family metadata
familyMetadata: {
encrypted: true,
data: String, // Encrypted metadata (address, phone, etc.)
iv: String,
authTag: String
},
// Plaintext family structure (for queries)
members: [
{
userId: String, // Plaintext (for queries)
profileId: String, // Plaintext (for queries)
role: String, // Plaintext (parent, child, elderly)
permissions: [String] // Plaintext (for JWT)
}
]
}
Encryption Notes:
- ✅ Plaintext:
familyId,members[*].userId,members[*].profileId,members[*].role,members[*].permissions - ❌ Encrypted:
familyName,familyMetadata
3. Profiles Collection
Purpose: Person profiles (users can have multiple profiles)
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
profileId: { type: String, unique: true, required: true },
userId: { type: String, index: true, required: true }, // Owner
familyId: { type: String, index: true },
profileType: { type: String }, // self, child, elderly, pet
// Encrypted profile data (privacy-preserving)
profileName: {
encrypted: true,
data: String, // Encrypted name (e.g., "John Doe")
iv: String,
authTag: String
},
// Encrypted profile metadata
profileMetadata: {
encrypted: true,
data: String, // Encrypted metadata (birth date, gender, etc.)
iv: String,
authTag: String
},
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
}
Encryption Notes:
- ✅ Plaintext:
profileId,userId,familyId,profileType - ❌ Encrypted:
profileName,profileMetadata
4. Health Data Collection
Purpose: Health records (weight, height, blood pressure, etc.)
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
healthDataId: { type: String, unique: true, required: true },
userId: { type: String, index: true, required: true }, // Owner
profileId: { type: String, index: true, required: true }, // Subject
familyId: { type: String, index: true },
// Encrypted health data (value + metadata)
healthData: [
{
// Encrypted value + metadata
encrypted: true,
data: String, // Encrypted: { value: 10, type: "blood_pressure", unit: "mmHg", date: "2026-02-14" }
iv: String,
authTag: String
}
],
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
dataSource: String // Plaintext (e.g., "manual", "healthKit", "googleFit")
}
Example: Blood Pressure Reading:
// Client-side data structure
const healthData = {
value: "120/80",
type: "blood_pressure",
unit: "mmHg",
date: "2026-02-14T10:30:00Z",
notes: "After morning coffee"
};
// Client encrypts healthData
const encryptedHealthData = encrypt(healthData, userKey);
// Stored in MongoDB
{
_id: ObjectId("..."),
healthDataId: "health-123",
userId: "user-456",
profileId: "profile-789",
familyId: "family-012",
// Encrypted (value + metadata)
healthData: [
{
encrypted: true,
data: "a1b2c3d4...", // Contains: value, type, unit, date, notes
iv: "e5f6g7h8...",
authTag: "i9j0k1l2..."
}
],
// Metadata (plaintext)
createdAt: ISODate("2026-02-14T10:30:00Z"),
updatedAt: ISODate("2026-02-14T10:30:00Z"),
dataSource: "healthKit"
}
Encryption Notes:
- ✅ Plaintext:
healthDataId,userId,profileId,familyId,createdAt,updatedAt,dataSource - ❌ Encrypted:
healthData[*](value + metadata)
5. Lab Results Collection
Purpose: Lab test results (imported via QR code)
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
labResultId: { type: String, unique: true, required: true },
userId: { type: String, index: true, required: true }, // Owner
profileId: { type: String, index: true, required: true }, // Subject
familyId: { type: String, index: true },
// Encrypted lab data (value + metadata)
labData: {
encrypted: true,
data: String, // Encrypted: { testType: "blood_test", results: [...], date: "...", lab: "..." }
iv: String,
authTag: String
},
// Encrypted lab metadata
labMetadata: {
encrypted: true,
data: String, // Encrypted: { labName: "LabCorp", doctor: "Dr. Smith", address: "..." }
iv: String,
authTag: String
},
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
dataSource: String // Plaintext (e.g., "qr_code", "manual_entry")
}
Example: Blood Test Results:
// Client-side data structure
const labData = {
testType: "blood_panel",
results: [
{ test: "cholesterol", value: 200, unit: "mg/dL", normalRange: "125-200" },
{ test: "glucose", value: 95, unit: "mg/dL", normalRange: "70-100" }
],
date: "2026-02-14T08:00:00Z",
lab: "LabCorp",
doctor: "Dr. Smith",
notes: "Fasting for 12 hours"
};
// Client encrypts labData + labMetadata
const encryptedLabData = encrypt(labData, userKey);
const encryptedLabMetadata = encrypt({ lab: "LabCorp", doctor: "Dr. Smith" }, userKey);
// Stored in MongoDB
{
_id: ObjectId("..."),
labResultId: "lab-123",
userId: "user-456",
profileId: "profile-789",
// Encrypted lab data (value + metadata)
labData: {
encrypted: true,
data: "m3n4o5p6...",
iv: "q7r8s9t0...",
authTag: "u1v2w3x4..."
},
// Encrypted lab metadata
labMetadata: {
encrypted: true,
data: "y5z6a7b8...",
iv: "c9d0e1f2...",
authTag: "g3h4i5j6..."
},
// Metadata (plaintext)
createdAt: ISODate("2026-02-14T08:00:00Z"),
updatedAt: ISODate("2026-02-14T08:00:00Z"),
dataSource: "qr_code"
}
Encryption Notes:
- ✅ Plaintext:
labResultId,userId,profileId,familyId,createdAt,updatedAt,dataSource - ❌ Encrypted:
labData(value + metadata),labMetadata
6. Medications Collection
Purpose: Medication tracking and reminders
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
medicationId: { type: String, unique: true, required: true },
userId: { type: String, index: true, required: true }, // Owner
profileId: { type: String, index: true, required: true }, // Subject
familyId: { type: String, index: true },
// Encrypted medication data (value + metadata)
medicationData: {
encrypted: true,
data: String, // Encrypted: { name: "Aspirin", dosage: "100mg", frequency: "daily", shape: "round" }
iv: String,
authTag: String
},
// Encrypted reminder schedule
reminderSchedule: {
encrypted: true,
data: String, // Encrypted: { times: ["08:00", "20:00"], days: ["mon", "tue", "wed", "thu", "fri"] }
iv: String,
authTag: String
},
// Metadata (plaintext)
active: { type: Boolean, default: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
}
Example: Medication:
// Client-side data structure
const medicationData = {
name: "Aspirin",
dosage: "100mg",
frequency: "daily",
shape: "round",
color: "white",
instructions: "Take with water after meals"
};
const reminderSchedule = {
times: ["08:00", "20:00"],
days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
notifications: true
};
// Client encrypts medicationData + reminderSchedule
const encryptedMedicationData = encrypt(medicationData, userKey);
const encryptedReminderSchedule = encrypt(reminderSchedule, userKey);
// Stored in MongoDB
{
_id: ObjectId("..."),
medicationId: "med-123",
userId: "user-456",
profileId: "profile-789",
// Encrypted medication data (value + metadata)
medicationData: {
encrypted: true,
data: "k9l0m1n2...",
iv: "o3p4q5r6...",
authTag: "s7t8u9v0..."
},
// Encrypted reminder schedule
reminderSchedule: {
encrypted: true,
data: "w1x2y3z4...",
iv: "a5b6c7d8...",
authTag: "e9f0g1h2..."
},
// Metadata (plaintext)
active: true,
createdAt: ISODate("2026-02-14T10:00:00Z"),
updatedAt: ISODate("2026-02-14T10:00:00Z")
}
Encryption Notes:
- ✅ Plaintext:
medicationId,userId,profileId,familyId,active,createdAt,updatedAt - ❌ Encrypted:
medicationData(value + metadata),reminderSchedule
7. Appointments Collection
Purpose: Medical appointments and checkups
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
appointmentId: { type: String, unique: true, required: true },
userId: { type: String, index: true, required: true }, // Owner
profileId: { type: String, index: true, required: true }, // Subject
familyId: { type: String, index: true },
// Encrypted appointment data (value + metadata)
appointmentData: {
encrypted: true,
data: String, // Encrypted: { type: "checkup", doctor: "Dr. Smith", date: "...", notes: "..." }
iv: String,
authTag: String
},
// Encrypted reminder settings
reminderSettings: {
encrypted: true,
data: String, // Encrypted: { reminder: 24, unit: "hours", method: "push_notification" }
iv: String,
authTag: String
},
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
}
Encryption Notes:
- ✅ Plaintext:
appointmentId,userId,profileId,familyId,createdAt,updatedAt - ❌ Encrypted:
appointmentData(value + metadata),reminderSettings
8. Shares Collection
Purpose: Time-limited access to shared data (from encryption.md)
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
shareId: { type: String, unique: true, required: true },
userId: { type: String, index: true, required: true }, // Owner
// References to original data
documentId: { type: String, required: true },
collectionName: { type: String, required: true }, // health_data, lab_results, etc.
// Encrypted shared data (encrypted with share-specific password)
encryptedData: {
encrypted: true,
data: String, // Encrypted with share-specific password
iv: String,
authTag: String
},
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
expiresAt: { type: Date, index: true },
accessCount: { type: Number, default: 0 },
maxAccessCount: { type: Number },
// Optional: Additional security
allowedEmails: [String],
isRevoked: { type: Boolean, default: false },
revokedAt: Date
}
Encryption Notes:
- ✅ Plaintext:
shareId,userId,documentId,collectionName,createdAt,expiresAt,accessCount,maxAccessCount,allowedEmails,isRevoked,revokedAt - ❌ Encrypted:
encryptedData(encrypted with share-specific password)
9. Refresh Tokens Collection
Purpose: JWT refresh token storage (from JWT authentication)
Schema:
{
_id: ObjectId("..."),
// Plaintext fields (for queries)
jti: { type: String, unique: true, required: true }, // JWT ID
userId: { type: String, index: true, required: true },
// Metadata (plaintext)
createdAt: { type: Date, default: Date.now },
expiresAt: { type: Date, index: true, required: true },
revoked: { type: Boolean, default: false },
revokedAt: Date
}
Encryption Notes:
- ✅ Plaintext:
jti,userId,createdAt,expiresAt,revoked,revokedAt - ❌ Encrypted: None (refresh tokens are not sensitive data)
Encryption Strategy
Client-Side Encryption (Before Sending to Server)
Encryption Flow:
// 1. User enters health data
const healthData = {
value: "120/80",
type: "blood_pressure",
unit: "mmHg",
date: "2026-02-14T10:30:00Z"
};
// 2. Client derives encryption key from password
const userKey = await deriveKeyFromPassword(userPassword);
// PBKDF2: 100,000 iterations, SHA-256, 32-byte key
// 3. Client encrypts health data
const encryptedHealthData = await encryptData(healthData, userKey);
// AES-256-GCM: 16-byte IV, auth tag for integrity
// 4. Client sends encrypted data to server
await fetch('/api/health-data', {
method: 'POST',
body: JSON.stringify({
userId: 'user-123',
profileId: 'profile-456',
familyId: 'family-789',
healthData: [encryptedHealthData] // Encrypted (value + metadata)
})
});
// 5. Server stores encrypted data in MongoDB
// Server NEVER decrypts data
Encryption Implementation (Client-Side)
React Native / React:
import * as crypto from 'crypto';
// Encrypted field structure
interface EncryptedField {
encrypted: true;
data: string; // Encrypted data
iv: string; // Initialization vector
authTag: string; // Authentication tag (AES-256-GCM)
}
// Encrypt data
async function encryptData(data: any, key: Buffer): Promise<EncryptedField> {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted: true,
data: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
// Decrypt data
async function decryptData(encryptedField: EncryptedField, key: Buffer): Promise<any> {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encryptedField.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedField.authTag, 'hex'));
let decrypted = decipher.update(encryptedField.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
// Derive key from password
async function deriveKeyFromPassword(password: string): Promise<Buffer> {
const salt = crypto.randomBytes(32);
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, 100000, 32, 'sha256', (err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey);
});
});
}
Indexing Strategy
Principle
Index ONLY plaintext fields (for performance and privacy).
Indexes per Collection
Users
db.users.createIndex({ userId: 1 }, { unique: true });
db.users.createIndex({ email: 1 }, { unique: true });
db.users.createIndex({ familyId: 1 });
db.users.createIndex({ createdAt: -1 });
Families
db.families.createIndex({ familyId: 1 }, { unique: true });
db.families.createIndex({ createdAt: -1 });
Profiles
db.profiles.createIndex({ profileId: 1 }, { unique: true });
db.profiles.createIndex({ userId: 1 });
db.profiles.createIndex({ familyId: 1 });
db.profiles.createIndex({ createdAt: -1 });
Health Data
db.health_data.createIndex({ healthDataId: 1 }, { unique: true });
db.health_data.createIndex({ userId: 1 });
db.health_data.createIndex({ profileId: 1 });
db.health_data.createIndex({ familyId: 1 });
db.health_data.createIndex({ createdAt: -1 });
db.health_data.createIndex({ updatedAt: -1 });
Lab Results
db.lab_results.createIndex({ labResultId: 1 }, { unique: true });
db.lab_results.createIndex({ userId: 1 });
db.lab_results.createIndex({ profileId: 1 });
db.lab_results.createIndex({ familyId: 1 });
db.lab_results.createIndex({ createdAt: -1 });
db.lab_results.createIndex({ updatedAt: -1 });
Medications
db.medications.createIndex({ medicationId: 1 }, { unique: true });
db.medications.createIndex({ userId: 1 });
db.medications.createIndex({ profileId: 1 });
db.medications.createIndex({ familyId: 1 });
db.medications.createIndex({ active: 1 });
db.medications.createIndex({ createdAt: -1 });
db.medications.createIndex({ updatedAt: -1 });
Appointments
db.appointments.createIndex({ appointmentId: 1 }, { unique: true });
db.appointments.createIndex({ userId: 1 });
db.appointments.createIndex({ profileId: 1 });
db.appointments.createIndex({ familyId: 1 });
db.appointments.createIndex({ createdAt: -1 });
db.appointments.createIndex({ updatedAt: -1 });
Shares
db.shares.createIndex({ shareId: 1 }, { unique: true });
db.shares.createIndex({ userId: 1 });
db.shares.createIndex({ expiresAt: 1 }); // For TTL index
db.shares.createIndex({ createdAt: -1 });
db.shares.createIndex({ isRevoked: 1 });
Refresh Tokens
db.refresh_tokens.createIndex({ jti: 1 }, { unique: true });
db.refresh_tokens.createIndex({ userId: 1 });
db.refresh_tokens.createIndex({ expiresAt: 1 }); // For TTL index
db.refresh_tokens.createIndex({ revoked: 1 });
TTL Indexes (Auto-Expiration)
// Shares: Auto-delete expired shares
db.shares.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 } // Delete immediately after expiration
);
// Refresh Tokens: Auto-delete expired tokens
db.refresh_tokens.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 } // Delete immediately after expiration
);
Privacy-Preserving Queries
Challenge
How to query encrypted data without decrypting it?
Solutions
1. Plaintext Queries (Recommended)
Query by plaintext fields only:
// GOOD: Query by plaintext fields
const healthData = await db.health_data.find({
userId: 'user-123', // Plaintext ✅
profileId: 'profile-456', // Plaintext ✅
familyId: 'family-789' // Plaintext ✅
}).toArray();
// Client decrypts healthData[i].healthData[j]
2. Tagging System (Encrypted Search)
Client adds searchable tags to encrypted data:
// Client adds tags to encrypted data
const healthData = {
value: "120/80",
type: "blood_pressure",
unit: "mmHg",
date: "2026-02-14T10:30:00Z",
tags: ["cardio", "daily"] // Plaintext tags (for client-side search)
};
// Stored in MongoDB
{
_id: ObjectId("..."),
healthDataId: "health-123",
userId: "user-123",
profileId: "profile-456",
// Encrypted health data
healthData: [
{
encrypted: true,
data: "a1b2c3d4...",
iv: "e5f6g7h8...",
authTag: "i9j0k1l2..."
}
],
// Plaintext tags (for client-side search)
tags: ["cardio", "daily"]
}
// Query by tags
const healthData = await db.health_data.find({
userId: 'user-123',
tags: { $in: ['cardio', 'daily'] } // Plaintext tags ✅
}).toArray();
// Client decrypts healthData[i].healthData[j]
3. Date Range Queries (Plaintext Dates)
Store dates as plaintext (for range queries):
// Client encrypts health data BUT stores date as plaintext
const healthData = {
value: "120/80",
type: "blood_pressure",
unit: "mmHg",
date: "2026-02-14T10:30:00Z" // Plaintext date ✅
};
// Stored in MongoDB
{
_id: ObjectId("..."),
healthDataId: "health-123",
userId: "user-123",
profileId: "profile-456",
// Plaintext date (for range queries)
date: ISODate("2026-02-14T10:30:00Z"), // Plaintext ✅
// Encrypted health data (without date)
healthData: [
{
encrypted: true,
data: "a1b2c3d4...", // Encrypted: { value, type, unit } (no date)
iv: "e5f6g7h8...",
authTag: "i9j0k1l2..."
}
]
}
// Query by date range
const healthData = await db.health_data.find({
userId: 'user-123',
date: {
$gte: ISODate("2026-02-01T00:00:00Z"),
$lte: ISODate("2026-02-28T23:59:59Z")
}
}).toArray();
// Client decrypts healthData[i].healthData[j]
Data Migration
Key Rotation
Strategy: Re-encrypt all data with new key
// 1. User changes password
const newPassword = "new-secure-password";
const newKey = await deriveKeyFromPassword(newPassword);
// 2. Client fetches all encrypted data
const healthData = await db.health_data.find({ userId: 'user-123' }).toArray();
// 3. Client decrypts with old key
const decryptedHealthData = healthData.map(d => ({
_id: d._id,
decrypted: await decryptData(d.healthData[0], oldKey)
}));
// 4. Client re-encrypts with new key
const reencryptedHealthData = decryptedHealthData.map(d => ({
_id: d._id,
encrypted: await encryptData(d.decrypted, newKey)
}));
// 5. Client sends re-encrypted data to server
for (const d of reencryptedHealthData) {
await db.health_data.updateOne(
{ _id: d._id },
{ $set: { healthData: [d.encrypted], updatedAt: new Date() } }
);
}
Performance Considerations
1. Encryption Overhead
- Client-side encryption: Minimal (10-50ms per encryption)
- Server-side storage: No overhead (encrypted data stored directly)
- Network transfer: Encrypted data is 20-30% larger than plaintext
2. Index Size
- Plaintext indexes: Smaller (only plaintext fields)
- Encrypted data: Not indexed (no performance impact)
3. Query Performance
- Plaintext queries: Fast (indexed fields)
- Tag-based queries: Fast (indexed plaintext tags)
- Date range queries: Fast (indexed plaintext dates)
- Encrypted data queries: Not possible (client-side filtering)
4. Storage Size
- Encrypted data: 20-30% larger than plaintext
- MongoDB storage: No impact (stores binary data)
Summary
Zero-Knowledge Encryption
- ✅ Client-side encryption: All sensitive data encrypted before reaching server
- ✅ Metadata encryption: Health data metadata (type, unit, etc.) also encrypted
- ✅ Plaintext queries: Query by plaintext fields (userId, profileId, familyId, date, tags)
- ✅ Server blindness: Server stores ONLY encrypted data, never decrypts
Collections
- ✅ Users: Authentication, profiles, family relationships
- ✅ Families: Family structure, encrypted family name/metadata
- ✅ Profiles: Person profiles, encrypted profile name/metadata
- ✅ Health Data: Encrypted health records (value + metadata)
- ✅ Lab Results: Encrypted lab data (value + metadata)
- ✅ Medications: Encrypted medication data + reminders
- ✅ Appointments: Encrypted appointment data + reminders
- ✅ Shares: Time-limited access to shared data
- ✅ Refresh Tokens: JWT refresh token storage
Privacy Preserved
- ✅ Blood pressure: Value + type + unit + date encrypted
- ✅ HIV test: Test type + result + date + doctor encrypted
- ✅ Cholesterol: Test type + result + date + lab encrypted
- ✅ All health data: Value + metadata encrypted
Next Steps
- Create MongoDB indexes
- Implement client-side encryption (React Native + React)
- Implement server-side API (Axum + MongoDB)
- Test encryption flow
- Test data migration (key rotation)
- Test privacy-preserving queries
- Performance testing