diff --git a/config/templates/org-member-added.html b/config/templates/org-member-added.html
new file mode 100644
index 000000000..12298e938
--- /dev/null
+++ b/config/templates/org-member-added.html
@@ -0,0 +1,8 @@
+
+
{{appName}} invitation
+
+ Hello {{displayName}},
+ You have been invited to join {{orgName}} on {{appName}}.
+ Review and accept the invitation here: {{url}}
+ The {{appName}} Team.
+
diff --git a/modules/organizations/controllers/organizations.membershipRequest.controller.js b/modules/organizations/controllers/organizations.membershipRequest.controller.js
index d53a9b0db..bd5f35486 100644
--- a/modules/organizations/controllers/organizations.membershipRequest.controller.js
+++ b/modules/organizations/controllers/organizations.membershipRequest.controller.js
@@ -143,6 +143,32 @@ const accept = async (req, res) => {
}
};
+/**
+ * @function decline
+ * @description Endpoint for the INVITED USER to decline a pending owner_add
+ * membership — deletes the row (the user can be re-invited later). Same
+ * auth-only surface and consent gate as accept: the service returns null on any
+ * mismatch (wrong user, a join_request, an already-active or unknown
+ * membership) → 404, never leaking which condition failed.
+ * @param {Object} req - Express request object
+ * @param {Object} res - Express response object
+ * @returns {Promise}
+ */
+const decline = async (req, res) => {
+ try {
+ const membership = await MembershipService.declineMembership(
+ req.params.membershipId,
+ req.user._id || req.user.id,
+ );
+ if (!membership) {
+ return responses.error(res, 404, 'Not Found', 'No pending invitation with that identifier has been found')();
+ }
+ responses.success(res, 'membership invitation declined')(membership);
+ } catch (err) {
+ responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
+ }
+};
+
/**
* @function requestByID
* @description Middleware to fetch a pending membership by its ID.
@@ -180,5 +206,6 @@ export default {
listMine,
listMinePending,
accept,
+ decline,
requestByID,
};
diff --git a/modules/organizations/repositories/organizations.membership.repository.js b/modules/organizations/repositories/organizations.membership.repository.js
index fa6e0cdee..e9eefe16c 100644
--- a/modules/organizations/repositories/organizations.membership.repository.js
+++ b/modules/organizations/repositories/organizations.membership.repository.js
@@ -79,6 +79,18 @@ const update = (membership) => membership.save().then((doc) => doc.populate(defa
*/
const remove = (membership) => Membership.deleteOne({ _id: membership.id || membership._id }).exec();
+/**
+ * @function findOneAndDelete
+ * @description Atomically find a single membership matching a filter and delete it.
+ * Returns the deleted document (populated) or null if no document matched, allowing
+ * callers to implement consent-gated deletes in a single round-trip that is free
+ * of read-then-delete race conditions.
+ * @param {Object} filter - The filter to match the document to delete.
+ * @returns {Promise