MongoDB Queryable Encryption: Keep Your Data Private While Still Searching It
The Problem Every Team Faces
You're building an app that stores sensitive user data like emails, SSNs, and credit cards. You have two bad options:
Enter Queryable Encryption: The Best of Both Worlds
MongoDB's Queryable Encryption lets you encrypt sensitive fields before they reach the database, keep using normal queries like find() and where(), and ensure MongoDB never sees your plaintext data, not even in memory or logs.
Think of it as end-to-end encryption for your database.
How It Works
When your application saves data, here's what happens behind the scenes:
Your app wants to save an email address like john@example.com. Before it reaches MongoDB, the driver automatically encrypts it into something like x7B#mK9$... that looks like complete gibberish. MongoDB stores only this encrypted version.
When you search for that email later, something clever happens. Your query for "john@example.com" gets encrypted into the exact same "x7B#mK9$..." pattern. MongoDB can match these encrypted values without ever knowing what they actually say. Your app then automatically decrypts the results back into readable text.
The magic here is that MongoDB can match encrypted values without ever decrypting them!
Let's Build It: 5-Step Setup
Prerequisites
You'll need MongoDB 7.0 or later, Node.js with Mongoose 8.15.0 or higher, and an AWS account for key management.
Step 1: Create Your Master Key in AWS
# Generate a secure key
MASTER_KEY=$(openssl rand -base64 96)
# Store it in AWS Secrets Manager
aws secretsmanager create-secret \
--name "mongodb-master-key" \
--secret-string "{\"key\": \"$MASTER_KEY\"}" \
--region us-east-1
Step 2: Set Up MongoDB Connection
const mongoose = require('mongoose');
async function connectDB() {
await mongoose.connect(process.env.MONGODB_URI, {
autoEncryption: {
keyVaultNamespace: "encryption.__keyVault",
kmsProviders: {
aws: {
secretAccessKeySelector: {
provider: "aws",
region: "us-east-1",
secretId: "mongodb-master-key" // Your AWS secret
}
}
}
}
});
console.log('Encrypted MongoDB connected successfully');
}
module.exports = connectDB;
Step 3: Define What to Encrypt
Here's the beautiful part. You just mark fields as encrypted in your schema:
// User.js
const userSchema = new mongoose.Schema({
// Not encrypted, safe to expose
name: String,
createdAt: Date,
// Encrypted and searchable
email: {
type: String,
encrypt: {
keyId: '/default',
queries: 'equality' // Can search with exact match
}
},
// Encrypted and searchable
ssn: {
type: String,
encrypt: {
keyId: '/default',
queries: 'equality'
}
},
// Encrypted with range queries
salary: {
type: Number,
encrypt: {
keyId: '/default',
queries: 'range' // Can search with >, <, between
}
}
}, {
encryptionType: 'queryableEncryption'
});
module.exports = mongoose.model('User', userSchema);
Step 4: Use It Like Normal MongoDB
Here's the amazing part. Your application code doesn't change:
// API routes work exactly as before
app.post('/users', async (req, res) => {
// Automatically encrypts email, ssn, salary
const user = await User.create(req.body);
res.json(user); // Automatically decrypts for response
});
app.get('/users/search', async (req, res) => {
// This STILL WORKS on encrypted data!
const user = await User.findOne({
email: req.query.email
});
res.json(user);
});
app.get('/users/high-earners', async (req, res) => {
// Range queries work on encrypted salary!
const users = await User.find({
salary: { $gte: 100000 }
});
res.json(users);
});
Recommended by LinkedIn
Step 5: Configure AWS Permissions
Create an IAM policy that only allows reading your secret:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:*:*:secret:mongodb-master-key-*"
}]
}
Attach this to your EC2 instance, Lambda function, or container role.
What Your Database Admin Sees
Without Encryption:
{
"name": "John Doe",
"email": "john@example.com",
"ssn": "123-45-6789",
"salary": 120000
}
With Queryable Encryption:
{
"name": "John Doe",
"email": "BinData(6, 'x7B#mK9$Qr...')",
"ssn": "BinData(6, 'pL@9nX2$Yz...')",
"salary": "BinData(6, 'mN$3kP8@Wx...')"
}
Even if someone steals your database, they get nothing useful!
When Should You Use This?
This approach works perfectly for healthcare apps storing patient records and insurance info, financial services handling account numbers and transactions, HR systems managing SSNs, salaries, and reviews, and any application that needs to meet GDPR, HIPAA, or PCI compliance requirements.
You can skip it for public data, fields you never search on (regular encryption is fine), and full-text search fields since they're not supported yet.
Performance Considerations
Equality queries typically add about 10 to 20 percent overhead, while range queries might add 30 to 40 percent. Storage requirements roughly double or triple for encrypted fields. For most applications, this overhead is negligible compared to the security gains.
Common Gotchas and Solutions
If your queries return nothing, ensure exact case matching since encryption is case-sensitive. Also check that your schema includes encryptionType: 'queryableEncryption'.
Wondering about encrypting existing data? You can't do it directly. You'll need to create a new encrypted collection and migrate your data.
Good news about indexes: you can create them on encrypted fields just like normal fields.
The Bottom Line
Queryable Encryption gives you something that seemed impossible: a database that can't read your sensitive data but can still search it efficiently.
In 5 minutes, you've learned how to keep MongoDB completely blind to your sensitive data, maintain full query capabilities, and meet compliance requirements without sacrificing developer experience.
Your users' data stays private. Your queries stay fast. Your compliance team stays happy.
Next step? Try it on a test collection. You'll be amazed that it just works.
Questions? Found this helpful? Let me know in the comments. Happy encrypting!
Very interesting! Thanks for sharing