Blog

Why I Used Prisma Client Extensions for CRUD Notifications (MSIS Perspective)

Cross-cutting notification emission on create/update/delete without breaking mutations, plus permission-aware recipients in CPMS.

Why I Used Prisma Client Extensions for CRUD Notifications (MSIS Perspective)
PMS1 min read2026-04-03
By Published Updated

Why I Used Prisma Client Extensions for CRUD Notifications (MSIS Perspective)

Direct answer (40 words): Prisma extensions hook all models for mutations in one place so audit-relevant events are emitted consistently; try/catch ensures availability of core business writes even if notification delivery fails—matching minimum security integrity expectations for procurement systems.

The problem

Sprinkling afterCreate logic across dozens of server actions creates inconsistent audit trails and missed events when a new model ships. Centralizing on the ORM client keeps one pipeline for “something changed in the domain.”

Implementation from this repo

Extension: mutate first, notify second

export const prisma = prismaBase.$extends({
  query: {
    $allModels: {
      async create({ model, args, query }) {
        const result = await query(args);
        try {
          await emitCrudNotifications(prismaBase, { model, operation: "create", args, result });
        } catch {
          // Notification writes must never break core mutation flow.
        }
        return result;
      },

The same pattern applies to update, updateMany, upsert, delete, and deleteMany—so deletes and bulk operations stay observable.

Notification routing maps models to permission paths

const MODEL_MODULE_PATHS: Record<string, string[]> = {
  Requisition: ["internalProcurement.requisitions"],
  PurchaseOrder: ["internalProcurement.purchaseOrders", "externalProcurement.purchaseOrders"],
  Invoice: ["internalProcurement.invoices", "externalProcurement.invoices"],
  Contract: ["internalProcurement.contracts", "externalProcurement.contracts"],
  Shipment: ["internalProcurement.shipments", "externalProcurement.shipments"],
  RFQ: ["externalProcurement.rfqs"],
  SupplierProfile: ["supplierManagement.suppliers"],
  SupplierCategory: ["supplierManagement.categories"],
  SupplierPerformanceRecord: ["supplierManagement.performance"],
  SupplierDocument: ["supplierManagement.sustainability", "documents"],
  SupplierDocumentCategory: ["supplierManagement.categories", "documents"],
  Budget: ["financials.budgets"],
  User: ["administration.users"],
  Department: ["administration.departments"],
  Currency: ["administration.currencies"],
  Role: ["administration.roles"],
  Record: ["records"],
  RecordCategoryModel: ["records"],
  Archive: ["archives"],
};

Recipients are resolved by users whose JSON permissions include read on the relevant module paths—aligning notifications with RBAC rather than broadcasting to everyone.

MSIS-aligned takeaway

  • Integrity: Core procurement writes commit even if notification persistence fails.
  • Traceability: Every supported mutation path triggers the same notification pipeline.
  • Least privilege: Recipients are filtered by read permission on the affected module.

Read the full case study: CPMS — Comprehensive Procurement Management System

Related reading

Why I Used JSON-Path RBAC and JWT Session Refresh in Auth.js v5 (Next.js 15)

Nested permission JSON, dot-path checks, legacy role fallbacks, and inactivity-aware JWT callbacks for a procurement CPMS.

Continue reading

How I Implemented Secure S3 Presigned URLs and Server Uploads for Procurement Documents

S3-compatible uploads with sanitized keys, server-side PutObject, and presigned GET for invoices and supplier documents in a Next.js App Router CPMS.

Continue reading

How I Implemented Department-Scoped Approval Workflows for Requisitions

Amount thresholds, manager vs finance gates, and department matching in a pure TypeScript workflow engine for CPMS.

Continue reading