Storing sensitive fields like email addresses, salaries, tokens, or PII in MongoDB comes with an obvious risk if the database is compromised, plain-text values are exposed immediately. This article shows how to solve this with a simple Mongoose plugin.
mongoose-aes-encryption provides transparent field-level AES-256-GCM encryption-at-rest — your application code reads and writes plain values as usual, while only ciphertext ever touches the database.
Table of Contents
The problem
Consider a typical user schema with sensitive data:
const schema = new mongoose.Schema({
username: { type: String },
email: { type: String },
salary: { type: Number },
phoneNumbers: { type: [String] }
});Without encryption, MongoDB stores every value in plain text. Anyone with read access to the database — a stolen backup, a misconfigured replica, a rogue admin — can read all of it instantly.
Setup of Mongoose AES-GCM encryption
Install the package:
npm install mongoose-aes-encryptionGenerate a 32-byte key and store it securely (environment variable, secrets manager — never hardcode it):
export ENCRYPTION_KEY=$(openssl rand -hex 32)Create the plugin once at application startup:
const createAESPlugin = require('mongoose-aes-encryption');
const plugin = createAESPlugin({ key: process.env.ENCRYPTION_KEY });Basic usage
Add encrypted: true to any field and apply the plugin to the schema — that’s it:
const schema = new mongoose.Schema({
username: { type: String },
email: { type: String, encrypted: true },
salary: { type: Number, encrypted: true },
phoneNumbers: { type: [String], encrypted: true }
});
schema.plugin(plugin);
const User = mongoose.model('User', schema);The rest of your application code remains completely unchanged:
// Write plain values — mongoose-aes-encryption encrypts transparently on save
const user = new User({ username: 'alice', email: 'alice@example.com', salary: 75000 });
await user.save();
// MongoDB stores: { email: '<iv|ciphertext|authTag>', salary: '<iv|ciphertext|authTag>', ... }
// Read plain values — decrypted transparently on load
const found = await User.findOne({ username: 'alice' });
console.log(found.email); // 'alice@example.com'
console.log(found.salary); // 75000Supported field types
All common Mongoose scalar types and arrays are supported:
String,Number,Date,Boolean[String],[Number]arrays- Inline nested sub-documents
- Separate sub-schemas (apply the plugin to both schemas)
Lean queries and manual operations
Mongoose .lean() bypasses getters and returns raw ciphertext. Decrypt manually using the exported decrypt function:
const { decrypt } = require('mongoose-aes-encryption');
const key = process.env.ENCRYPTION_KEY;
const doc = await User.findOne({ username: 'alice' }).lean();
const email = decrypt(doc.email, { key });
const salary = parseFloat(decrypt(doc.salary, { key }));Similarly, operations that bypass the Mongoose document lifecycle (updateOne, findOneAndUpdate, $set, etc.) require pre-encrypting values manually using the exported encrypt function before passing them to the query.
Conclusion
mongoose-aes-encryption delivers field-level AES-256-GCM encryption with tamper detection for Mongoose schemas in two lines of setup and one flag per field. It has zero production dependencies — the Node.js built-in crypto module does all the work. Existing application logic requires no changes for standard create/read/save operations.
Happy coding 🙂