You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'm not sure whether this should be a core feature or rather be a pattern that's easy to be implemented in "user land" e.g. by using scoped queries.
Reacted by Meotimdihia, Jared Fraser, Lars Larsson, Jaroslav Málek, gatchan, Stanislav Shelemekh, David Brett, Sardorkhuja Tukhtakhodjaev, Afsah Nasir, Hernán Cano and 19 more
I have a slight preference for this being a userland thing, as I've definitely mixed "hard delete" and "soft delete" models in the same system depending on product requirements (e.g. soft delete Article records, but hard delete Tag records)
Reacted by Johannes Schickling, Matt Mueller, Ben Seitz, JD Angerhofer, Adam Duro, Joël Galeran, Eugenio Berretta, chrissisura, Jacob Clarke, Hervé Labas and 7 more
Some kind of mechinism to support this would be a big help.
Reacted by Bruno Borges, Gabriel, Harry Wynn-Williams, Eugenio Berretta, chrissisura, Stanislav Shelemekh, David Brett, Sardorkhuja Tukhtakhodjaev, AlanDiazCochair, Kareem Ebrahim and 2 more
Reacted by Harry Wynn-Williams, TJ, Eugenio Berretta, ExLucas, Jacob Coker-Dukowitz, Vitor Savedra, Ajit Panigrahi, Sebastian Schubotz, Teneng Clifford and n年级
Maybe we could have the ability to tag a model with something like @@softDelete or a field with @deleteAt in the prisma schema?
Reacted by Gabriel, Eugenio Berretta, Tristan Siegel, Kai, Vinit Sarvade, Ioannis Antoniou, David Brett, Jacob Coker-Dukowitz, Rubek Joshi, HIROKAZU uchiyama and 16 more
This can be possibly be created using a middleware for now(thanks @mhwelander for the example)
import{PrismaClient}from"@prisma/client";constprisma=newPrismaClient({});asyncfunctionmainAsync(){prisma.$use(async(params,next)=>{// Check incoming query typeif(params.model=="Post"&¶ms.action=="delete"){// Change action to an updateparams.action="update";// Set field valueparams.args["data"]={deleted: true};}returnnext(params);});constcreatePost=awaitprisma.post.create({data: {title: "How to use Prisma Client"},});constdeletePost=awaitprisma.post.delete({where: {id: createPost.id,},});constgetPost=awaitprisma.post.findOne({where: {id: deletePost.id,},});console.log(getPost);// Will return post with deleted:true}mainAsync();
Reacted by Grzegorz Widła, Dario Ielardi, alaa, Hervé Labas, Ricardo Canastro, Josias Iquabius, Rubek Joshi, PotatoHD404, Charles, notDemian and 3 more
The soft delete feature has been a topic of discussion for many years now, from 2019 to 2025. Could you let us know when this feature might be built-in? Currently, implementing soft delete manually often leads to the following issues:
Each project needs to implement its own soft delete logic, resulting in a lot of repetitive code.
Query filters (e.g., WHERE deletedAt IS NULL) need to be manually added to every query.
Different teams implement this functionality in varying ways, making the codebase difficult to maintain.
I sincerely hope this functionality can be built-in. I believe it's not just one development team using Prisma that has encountered this issue. I look forward to feedback from the Prisma ORM team regarding this feature suggestion. I am willing to provide more use cases or participate in related discussions to help move this feature forward.
Reacted by Gabriel Haruki, Mykal, LuxQuant, OverPhoenix, Krishna Adhikari, Le Thanh Nhan, hlaporthein and n年级
If it's too difficult to implement soft delete functionality for all mutation queries (UPDATE, INSERT, DELETE) can we at the very least have way to add soft delete to all where calls, regardless it is a top-level where or a nested where or an include..where?
Because I personally do not need the ORM to handle just setting the delete field for me. That is easily accomplished with UPDATE by myself.
What is hard is not peppering the whole codebase with WHERE deletedAt = null.
I feel that will significantly improve the ergonomics of the framework. For example:
Prisma.defineExtension({name: "softDelete",// proposed new middleware feature just for modifying WHERE clauses, and nothing else// to be used by the framework for all WHERE filters, regardless it's a top-level WHERE, nested WHERE, or include (JOIN)globalQueryFilter: {$allModels: ({ model, where }){if(where.deletedAt){returnwhere;}return{ ...where,deletedAt: null}}}});
@ryanelian - as you've demonstrated at least somewhat, this alone is pretty easy to achieve with client extensions, especially if you go on to implement nested structures.
For us, we are already using a repository pattern, so it's fairly straightforward to centralize adding deletedAt: null to where clauses. I highly recommend it, you will probably find more use cases than just this for centralizing your query logic.
The bigger issue we face is data integrity. Since there is no support for partial unique indexes (a UNIQUE index only on not-soft-deleted records), unique fields in a soft-delete model are virtually impossible to implement on the database layer. As a result we have to constantly check for uniqueness manually, which opens us up to the potential for a lot more bugs and also blocks us from using much better native Postgres functionality.
Unfortunately, the Prisma team appears to have closed most of the open requests for this functionality as "closed / not planned". You can just add the index yourself manually, but you won't be able to use any upsert / whereUnique functionality - so performance on certain operations will suffer - and your index will be repeatedly deleted every time you run prisma migrate. They refuse to implement workarounds for these issues as well.
In general, Prisma is just not-so-subtly hostile to the idea of soft deletes. I suspect they are philosophically opposed to soft deletes, otherwise they might try to open up some kind of pathway towards using them in a consistent and straightforward manner (after all, it has to be one of the most common use cases for any ORM). If you want to work with Prisma, you'll have to embrace a fully own-baked (probably half-baked) solution.
I am struggling to understand how a feature this critical has not been prioritised? Offloading the implementation of this functionality to the developer, particularly when "per entity" granularity (not a global middleware) is required is a massive time sink, especially for large teams where legal compliance and recoverability are a requirement, which is many if not most companies.
Reacted by Dmitriy, gxrxrdx, William Rheinheimer, redundantautomata, AllHailCheese, LuxQuant, OverPhoenix, Ken So, Rafaell Lycan, João Amorim and 22 more
I don't think this is something Prisma should directly add to the core. The logic depends on how exactly you want the soft-delete filter to work and how you apply it. We could easily implement a helper to recursively add the where: { deletedAt: null } filter using the dmmf.datamodel that is exposed by the generated prisma client. Then we could use it in the lifecycle hooks to apply the constraints to the args for all models/operations (or the ones you want selectively).
import _ from "lodash";
import { Prisma } from "@/prisma/generated/client";
interface PrismaArgs {
include?: any;
select?: any;
where?: any;
[x: string]: any;
}
export const DELETED_AT_FIELD = "deletedAt";
function withSoftDeleteCondition<T extends PrismaArgs>(args: T): T {
return {
...args,
where: {
...args.where,
[DELETED_AT_FIELD]: null,
},
};
}
function getPopulateArgs<T extends PrismaArgs>(args: T, argsPath: string): T {
if (!argsPath)
return args;
const populateArgs = _.get(args, argsPath);
// If it's an object, return it
if (typeof populateArgs === "object" && populateArgs !== null) {
return populateArgs;
}
// If 'true', null, undefined, or primitive, return empty object
return {} as T;
}
function applySoftDeleteFilterToArgs<T extends PrismaArgs>(args: T, argsPath: string): T {
const populateArgs = getPopulateArgs(args, argsPath);
const populateArgsWithDeleteCondition = withSoftDeleteCondition(populateArgs);
if (argsPath) {
return _.set(args, argsPath, populateArgsWithDeleteCondition);
}
return populateArgsWithDeleteCondition;
}
const getPopulateKey = (args: PrismaArgs) => args.include ? "include" : "select";
function addRecursiveSoftDeleteFilter<T extends PrismaArgs>(
modelName: Prisma.ModelName,
operation: string | null,
args: T,
argsPath = "",
): T {
const model = Prisma.dmmf.datamodel.models.find(m => m.name === modelName);
if (!model) {
throw new Error(`Model "${modelName}" not found.`);
}
const isCreateOperation = operation === "create" || operation === "createMany" || operation === "createManyAndReturn";
const isUpdateOperation = operation === "update" || operation === "updateMany" || operation === "updateManyAndReturn" || operation === "upsert";
const isRestoreOperation = isUpdateOperation && args.data?.[DELETED_AT_FIELD] === null;
// Skip 'createOperations', as they don't allow top level 'where' clause.
// Also skip 'restoreOperations', with data that contains the `deleteAt: null` which is an operation to remove the 'deletedAt' value.
if (!isCreateOperation && !isRestoreOperation) {
// Apply filter to the current model level
for (const field of model.fields) {
if (field.name === DELETED_AT_FIELD) {
args = applySoftDeleteFilterToArgs(args, argsPath);
break;
}
}
}
// Recurse into included/selected relations
const populateArgs = getPopulateArgs(args, argsPath);
const populateKey = getPopulateKey(populateArgs);
const populateObject = populateArgs[populateKey];
// Check if 'populateObject' exists
if (!populateObject || typeof populateObject !== "object") {
return args;
}
// Iterate keys of the populate object
for (const key of Object.keys(populateObject)) {
const field = model.fields.find(f => f.name === key);
// Check if it's a relation (kind: 'object'). 1 to 1 required relations have no 'where' clause, so we only add soft-delete to to-many and optional relations.
if (!field || field.kind !== "object" || (!field.isList && field.isRequired)) {
continue;
}
const populateArgsKey = argsPath ? `${argsPath}.${populateKey}.${key}` : `${populateKey}.${key}`;
// Recurse: This mutates 'args' object by reference
addRecursiveSoftDeleteFilter(field.type as Prisma.ModelName, null, args, populateArgsKey);
}
return args;
}
/**
* Recursively applies the 'deletedAt: null' filter to the current model and
* all supported relational fields (includes/selects) that support a 'where' clause.
*
* @template T
* @param {Prisma.ModelName} model - The PascalCase name of the current model (e.g., "User").
* @param {string | null} [operation] - The operation type (e.g., 'findMany').
* @param {T} args - The root Prisma arguments object. This object is MUTATED by reference.
* @returns {T} The mutated arguments object with soft-delete filters applied.
*/
export function applySoftDeleteConstraints<T extends PrismaArgs>(
model: Prisma.ModelName,
operation: string,
args: T,
): T {
return addRecursiveSoftDeleteFilter(model, operation, args);
}
Use middleware or client extensions to hook into query lifecycles and apply the constraints to args before they execute. Here I'm also overriding the delete and deleteMany to turn them into update operations instead, so when using a query like prisma.user.delete() it will actually run a prisma.user.update() instead that will set the deletedAt to the current datetime.
const prisma = new PrismaClient().$extends({
query: {
// Use $allModels to apply to every model
$allModels: {
// Apply soft-delete constraints to all operations
$allOperations({ model, operation, args, query }) {
args = applySoftDeleteConstraints(model as Prisma.ModelName, operation, args);
return query(args);
},
// #region Override 'delete' and 'deleteMany' to transform these into 'update' operations.
async delete({ model, args }): Promise<any> {
return (prisma as any)[model].update({
...args,
data: {
[DELETED_AT_FIELD]: new Date(),
},
});
},
async deleteMany({ model, args }): Promise<any> {
return (prisma as any)[model].updateMany({
...args,
data: {
[DELETED_AT_FIELD]: new Date(),
},
});
},
// #endregion
},
},
});
Reacted by Teneng Clifford and Gerison Kimathi Muriungi
I’m the author of prisma-paranoid. I developed this plugin to simplify how we handle soft deletes in Prisma, making the "paranoid" behavior feel more native and automated.
The main goal is to avoid the boilerplate of manually adding { deletedAt: null } filters to every single query. It leverages Prisma’s $extends client and generated metadata to handle everything under the hood.
1. Schema Setup
Add the generator and the paranoid field to your schema.prisma:
generatormetadata {provider="prisma-metadata-generator"output="./prisma/generated/metadata.ts"}modelUser {idString@id@default(uuid())emailString@uniquedeletedAtDateTime?// default paranoid field name}
Important
Remember to run the generator to update your metadata:
This issue has been open since 2019. If this feature is not planned, it might be better to close the issue. Otherwise, it would be helpful to have an update on its status
Activity
schickling commentedon Jun 27, 2019
I'm not sure whether this should be a core feature or rather be a pattern that's easy to be implemented in "user land" e.g. by using scoped queries.
nelsonpecora commentedon Jul 11, 2019
I have a slight preference for this being a userland thing, as I've definitely mixed "hard delete" and "soft delete" models in the same system depending on product requirements (e.g. soft delete Article records, but hard delete Tag records)
duro commentedon Jun 6, 2020
Some kind of mechinism to support this would be a big help.
leohxj commentedon Aug 16, 2020
something like GORM, if model have deleteAt field, it auto have soft_delete ability.
http://gorm.io/docs/delete.html
Hebilicious commentedon Aug 17, 2020
Maybe we could have the ability to tag a model with something like
@@softDeleteor a field with@deleteAtin the prisma schema?LinboLen commentedon Aug 18, 2020
It shoud be a core feature
carlosfiori commentedon Sep 4, 2020
It would be great
pantharshit00 commentedon Sep 7, 2020
This can be possibly be created using a middleware for now(thanks @mhwelander for the example)
64 remaining items
zzzhim commentedon Mar 16, 2025
The soft delete feature has been a topic of discussion for many years now, from 2019 to 2025. Could you let us know when this feature might be built-in? Currently, implementing soft delete manually often leads to the following issues:
I sincerely hope this functionality can be built-in. I believe it's not just one development team using Prisma that has encountered this issue. I look forward to feedback from the Prisma ORM team regarding this feature suggestion. I am willing to provide more use cases or participate in related discussions to help move this feature forward.
ryanelian commentedon Mar 24, 2025
If it's too difficult to implement soft delete functionality for all mutation queries (
UPDATE,INSERT,DELETE) can we at the very least have way to add soft delete to allwherecalls, regardless it is a top-levelwhereor a nestedwhereor aninclude..where?Because I personally do not need the ORM to handle just setting the delete field for me. That is easily accomplished with
UPDATEby myself.What is hard is not peppering the whole codebase with
WHERE deletedAt = null.I feel that will significantly improve the ergonomics of the framework. For example:
Prior arts: https://learn.microsoft.com/en-us/ef/core/querying/filters
From this foundation, the Prisma team can iterate on the full soft delete feature if desired.
jove4015 commentedon Mar 25, 2025
@ryanelian - as you've demonstrated at least somewhat, this alone is pretty easy to achieve with client extensions, especially if you go on to implement nested structures.
For us, we are already using a repository pattern, so it's fairly straightforward to centralize adding deletedAt: null to where clauses. I highly recommend it, you will probably find more use cases than just this for centralizing your query logic.
The bigger issue we face is data integrity. Since there is no support for partial unique indexes (a UNIQUE index only on not-soft-deleted records), unique fields in a soft-delete model are virtually impossible to implement on the database layer. As a result we have to constantly check for uniqueness manually, which opens us up to the potential for a lot more bugs and also blocks us from using much better native Postgres functionality.
Unfortunately, the Prisma team appears to have closed most of the open requests for this functionality as "closed / not planned". You can just add the index yourself manually, but you won't be able to use any upsert / whereUnique functionality - so performance on certain operations will suffer - and your index will be repeatedly deleted every time you run prisma migrate. They refuse to implement workarounds for these issues as well.
In general, Prisma is just not-so-subtly hostile to the idea of soft deletes. I suspect they are philosophically opposed to soft deletes, otherwise they might try to open up some kind of pathway towards using them in a consistent and straightforward manner (after all, it has to be one of the most common use cases for any ORM). If you want to work with Prisma, you'll have to embrace a fully own-baked (probably half-baked) solution.
yisibl commentedon Apr 20, 2025
This is easily done in Sequelize with the
paranoidoption.https://sequelize.org/docs/v7/models/paranoid/
SemajDraw commentedon May 4, 2025
I am struggling to understand how a feature this critical has not been prioritised? Offloading the implementation of this functionality to the developer, particularly when "per entity" granularity (not a global middleware) is required is a massive time sink, especially for large teams where legal compliance and recoverability are a requirement, which is many if not most companies.
haZya commentedon Oct 27, 2025
I don't think this is something Prisma should directly add to the core. The logic depends on how exactly you want the soft-delete filter to work and how you apply it. We could easily implement a helper to recursively add the
where: { deletedAt: null }filter using the dmmf.datamodel that is exposed by the generated prisma client. Then we could use it in the lifecycle hooks to apply the constraints to the args for all models/operations (or the ones you want selectively).Use middleware or client extensions to hook into query lifecycles and apply the constraints to args before they execute. Here I'm also overriding the
deleteanddeleteManyto turn them intoupdateoperations instead, so when using a query likeprisma.user.delete()it will actually run aprisma.user.update()instead that will set thedeletedAtto the current datetime.minhtan143 commentedon Dec 8, 2025
@haZya There are many issues beyond queries, like handling soft deletes for tables with
@uniquecolumns andfindUnique,upsertrelatedgerkim62 commentedon Dec 26, 2025
Unfortunately prisma is no longer exposing DMMF, which makes implementing this in userland even harder. The recommended client extension no longer works for the new prisma-client-js in Prisma@7. Please consider removing the status/has-client-extension
EduardMcfly commentedon Feb 15, 2026
Hi everyone!
I’m the author of prisma-paranoid. I developed this plugin to simplify how we handle soft deletes in Prisma, making the "paranoid" behavior feel more native and automated.
The main goal is to avoid the boilerplate of manually adding
{ deletedAt: null }filters to every single query. It leverages Prisma’s$extendsclient and generated metadata to handle everything under the hood.1. Schema Setup
Add the generator and the paranoid field to your
schema.prisma:Important
Remember to run the generator to update your metadata:
2. Client Extension
Then, simply extend your Prisma client:
arentalb commentedon Mar 15, 2026
This issue has been open since 2019. If this feature is not planned, it might be better to close the issue. Otherwise, it would be helpful to have an update on its status
chbybnwr commentedon Mar 15, 2026
A workaround is to use row-level security, if your database can use it.