A Mongoose plugin for soft deletes with paranoid mode
npm install @jsupa/mongoose-paranoiaA powerful Mongoose plugin that adds soft delete functionality with paranoid mode support. Keep your data safe while maintaining a clean database interface.


- ๐ก๏ธ Soft Delete - Mark records as deleted without actually removing them
- ๐ Flexible Query Modes - Three filtering strategies to fit your needs
- ๐ Timestamp Tracking - Automatically record when items were deleted
- ๐ค User Tracking - Optional field to track who deleted the record
- ๐ Easy Restoration - Built-in methods to restore soft-deleted records
- ๐ TypeScript Support - Full type safety with TypeScript
- ๐ฏ Zero Config - Works out of the box with sensible defaults
``bashFrom GitHub Packages
npm install @jsupa/mongoose-paranoia
๐ Quick Start
`typescript
import mongoose from 'mongoose';
import Paranoia from '@jsupa/mongoose-paranoia';// Define your schema
const userSchema = new mongoose.Schema({
name: String,
email: String
});
// Add the plugin
userSchema.plugin(Paranoia);
// Create your model
const User = mongoose.model('User', userSchema);
// Use it like normal - deletes are now soft!
await User.deleteOne({ email: 'user@example.com' }); // Soft delete
const users = await User.find(); // Only returns non-deleted users
`๐ Usage
$3
`typescript
import mongoose, { Schema, Model } from 'mongoose';
import Paranoia, {
type ParanoiaDocument,
type ParanoiaQueryHelpers,
type ParanoiaStatics
} from '@jsupa/mongoose-paranoia';interface IUser extends ParanoiaDocument {
name: string;
email: string;
}
const userSchema = new Schema<
IUser,
Model & ParanoiaStatics,
{},
ParanoiaQueryHelpers
>({
name: String,
email: String
});
userSchema.plugin(Paranoia, {
deletedAt: true, // Add deletedAt timestamp
deletedBy: false, // Don't track who deleted
activeArchive: 'Default' // Auto-filter deleted records
});
const User = mongoose.model & ParanoiaStatics>('User', userSchema);
`$3
`typescript
interface ParanoiaOptions {
// Enable deletedAt timestamp field (default: true)
deletedAt?: boolean;
// Enable deletedBy field to track who deleted (default: false)
deletedBy?: boolean;
// Type of deletedBy field: 'ObjectId' or 'String' (default: 'ObjectId')
deletedByType?: string;
// Filtering strategy (default: 'Default')
// - "Default": Auto-filter deleted records (use .withDeleted() to include)
// - "Scope": Must explicitly use .active() or .deleted()
// - "All": Return all records by default (use .active() to filter)
activeArchive?: 'Scope' | 'Default' | 'All';
// Customize field names
deletedField?: string; // default: 'deleted'
deletedAtField?: string; // default: 'deletedAt'
deletedByField?: string; // default: 'deletedBy'
}
`๐ฏ Active Archive Modes
$3
Automatically filters out deleted records. Use
.withDeleted() to include them.`typescript
userSchema.plugin(Paranoia, { activeArchive: 'Default' });// Only returns non-deleted users
const activeUsers = await User.find();
// Returns all users including deleted
const allUsers = await User.find().withDeleted();
// Only returns deleted users
const deletedUsers = await User.find().deleted();
`$3
Must explicitly use query helpers for filtering.
`typescript
userSchema.plugin(Paranoia, { activeArchive: 'Scope' });// Returns ALL users (including deleted)
const allUsers = await User.find();
// Returns only active users
const activeUsers = await User.find().active();
// Returns only deleted users
const deletedUsers = await User.find().deleted();
`$3
Returns everything by default. Use
.active() to filter.`typescript
userSchema.plugin(Paranoia, { activeArchive: 'All' });// Returns ALL users
const allUsers = await User.find();
// Returns only active users
const activeUsers = await User.find().active();
`๐จ API Reference
$3
All query helpers work with
find(), findOne(), countDocuments(), and aggregate():`typescript
// Get only active (non-deleted) records
const active = await User.find().active();// Get only deleted records
const deleted = await User.find().deleted();
// Get all records (bypass default filtering)
const all = await User.find().withDeleted();
`$3
All standard Mongoose delete operations are converted to soft deletes:
`typescript
// Delete one document
await User.deleteOne({ email: 'user@example.com' });// Delete multiple documents
await User.deleteMany({ inactive: true });
// Find and delete
await User.findOneAndDelete({ _id: userId });
await User.findByIdAndDelete(userId);
`$3
`typescript
// Restore a single document (instance method)
const user = await User.findById(userId).withDeleted();
await user.restore();// Restore multiple documents (static method)
await User.restore({ email: { $in: emailList } });
`$3
Every document has these additional fields:
`typescript
interface ParanoiaDocument {
deleted: boolean; // Indicates if soft-deleted
deletedAt?: Date; // Timestamp of deletion (if enabled)
deletedBy?: any; // Who deleted it (if enabled)
restore(): Promise; // Restore the document
}
`๐จ Advanced Examples
$3
`typescript
userSchema.plugin(Paranoia, {
deletedBy: true,
deletedByType: 'ObjectId' // or 'String'
});// You'll need to manually set deletedBy in your delete logic
const deletedUser = await User.findByIdAndUpdate(userId, {
deleted: true,
deletedAt: new Date(),
deletedBy: currentUserId
});
`$3
`typescript
userSchema.plugin(Paranoia, {
deletedField: 'isArchived',
deletedAtField: 'archivedAt',
deletedByField: 'archivedBy'
});
`$3
`typescript
// In Default mode, deleted records are automatically filtered
const stats = await User.aggregate([
{ $group: { _id: '$status', count: { $sum: 1 } } }
]);// Include deleted records in aggregation
const allStats = await User.aggregate([
{ $group: { _id: '$status', count: { $sum: 1 } } }
]).withDeleted();
`๐ง TypeScript Support
Full TypeScript support with proper type definitions:
`typescript
import mongoose, { Model, Schema } from 'mongoose';
import Paranoia, {
type ParanoiaDocument,
type ParanoiaQueryHelpers,
type ParanoiaStatics
} from '@jsupa/mongoose-paranoia';interface IUser extends ParanoiaDocument {
name: string;
email: string;
}
const userSchema = new Schema<
IUser,
Model & ParanoiaStatics,
{},
ParanoiaQueryHelpers
>({
name: { type: String, required: true },
email: { type: String, required: true, unique: true }
});
userSchema.plugin(Paranoia);
const User = mongoose.model & ParanoiaStatics>('User', userSchema);
// Now you have full type safety!
`$3
The plugin exports several TypeScript helpers to make your code more type-safe:
####
SoftDeleteDocumentA type helper that combines your document interface with
ParanoiaDocument:`typescript
interface IUser {
name: string;
email: string;
}// Automatically includes: deleted, deletedAt, deletedBy, restore()
type UserDocument = SoftDeleteDocument;
`####
ParanoiaModelAn enhanced Model interface that includes all Paranoia plugin methods:
`typescript
type UserModel = ParanoiaModel;// Provides type-safe access to:
// - model.restore(filter)
// - All overridden delete methods
`####
ParanoiaQueryHelpersQuery helper interface for type-safe query methods:
`typescript
const userSchema = new Schema({
// schema definition
});// Provides type-safe access to:
// - query.active()
// - query.deleted()
// - query.withDeleted()
`$3
`typescript
import mongoose, { Schema, Model } from 'mongoose';
import Paranoia, {
type ParanoiaDocument,
type ParanoiaQueryHelpers,
type ParanoiaStatics
} from '@jsupa/mongoose-paranoia';// 1. Define your interface extending ParanoiaDocument
interface IUser extends ParanoiaDocument {
name: string;
email: string;
}
// 2. Define schema with full type safety
const userSchema = new Schema<
IUser,
Model & ParanoiaStatics,
{},
ParanoiaQueryHelpers
>({
name: { type: String, required: true },
email: { type: String, required: true, unique: true }
});
// 3. Add plugin
userSchema.plugin(Paranoia);
// 4. Create model
const User = mongoose.model & ParanoiaStatics>('User', userSchema);
// 5. Use with full type safety!
const user = await User.create({ name: 'John', email: 'john@example.com' });
await user.restore(); // โ
Type-safe instance method
await User.restore({ email: 'john@example.com' }); // โ
Type-safe static method
const activeUsers = await User.find().active(); // โ
Type-safe query helper
`๐งช Testing
`bash
Run tests
pnpm testRun tests in watch mode
pnpm test:watchRun tests with UI
pnpm test:ui
`๐ License
MIT ยฉ jsupa
๐ค Contributing
Contributions, issues, and feature requests are welcome!
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add some amazing feature')
4. Push to the branch (git push origin feature/amazing-feature`)Inspired by the need for better soft delete functionality in Mongoose applications.
See Releases for changelog.
---
Made with โค๏ธ by jsupa