Case Study

CPMS — Comprehensive Procurement Management System

French-first Next.js 15 procurement platform with Auth.js RBAC, Prisma on PostgreSQL, S3-compatible documents, and approval workflows across internal and external sourcing.

CPMS — Comprehensive Procurement Management System
By Published Updated

Comprehensive Procurement Management System (CPMS)

1. The 60-Word "AI Citation" Summary

CPMS is a French-first Next.js 15 (App Router) procurement platform spanning internal requisitions, external RFQs, contracts, logistics, budgets, and supplier governance. Auth.js v5 credentials sessions carry JSON RBAC with legacy-role fallbacks; Prisma 6 on PostgreSQL models the full lifecycle; S3-compatible storage with presigned URLs secures documents and invoices; next-intl localizes UI and PDFs; server actions enforce permissions; Prisma Client extensions emit permission-aware notifications on mutations—unifying operational and international sourcing in one auditable system.

2. The Power Stack Table

CategoryTechnologies
LanguagesTypeScript (strict), SQL (via Prisma)
FrameworkNext.js 15.1+, React 19, App Router (src/app)
AuthAuth.js (next-auth v5 beta), Credentials provider, bcrypt, JWT sessions with Prisma adapter
DataPostgreSQL (Railway), Prisma ORM 6.9
i18nnext-intl 3.x, messages/fr.json + messages/en.json, default locale fr
UITailwind CSS 4, Radix UI (shadcn-style), Lucide, next-themes, Recharts
Forms & validationreact-hook-form, Zod, @hookform/resolvers
Client stateZustand (persisted procurement mode + display currency, cookie sync)
DocumentsAWS SDK v3 (S3 + presigner), @react-pdf/renderer for PO PDFs
Infra (documented)Vercel (frontend), Railway (DB), S3-compatible object storage

3. The Challenge

Organizations running parallel internal (office needs) and external (import/trading) procurement often lose traceability: spreadsheets, email threads, and ad-hoc approvals cannot enforce who may approve what, budget guardrails, or supplier compliance in one place. CPMS targets unified operations, French-first legal and UX surfaces (with English secondary), and a supplier portal narrative for registration, bidding, and document lifecycle—without fragmenting data across tools.

4. Engineering Architecture

  • App Router + locale segments: Routes live under src/app/[locale]/… with next-intl middleware for locale prefixing (fr default, en secondary). Static params generation supports locale-aware layouts.
  • Auth at the edge of UI, enforcement in the server: Middleware handles i18n only; authentication and RBAC are applied in server layouts (redirect to login) and server actions ("use server") so every mutation re-validates the session—reducing “client-only” security gaps.
  • JWT + hydrated permissions: Sessions use JWT strategy with role and permission JSON embedded; refresh on update keeps permissions aligned with database Role.permissions, with legacy role maps when JSON is missing.
  • Domain modeling in Prisma: Enums and relations encode procurement states (requisitions, POs, RFQs, shipments, invoices, budgets). Cross-cutting notifications attach via Prisma Client extensions so feature code stays thin.
  • Binary assets: Uploads go to S3-compatible buckets; presigned GET limits exposure duration; keys are structured and sanitized server-side.

5. Three Technical Wins

Win A — JSON-path RBAC with legacy fallback

Permissions are stored as nested JSON and checked with a dot-path resolver; legacy role names still map to full defaults. This keeps admin UI flexible without scattering switch(role) across the codebase.

export function hasPermission(
  permissions: Permissions | null | undefined,
  path: string,
  action: PermissionAction
): boolean {
  if (!permissions) return false;
  const node = getByPath(permissions, path);
  if (!node || typeof node !== "object") return false;
  return Boolean((node as PermissionNode)[action]);
}

Win B — Approval workflow that combines amount rules and org context

A pure workflow engine encodes thresholds (e.g. manager-only approval for amounts under a cap); department manager vs approver-in-department is resolved separately from finance approval—reducing mistaken escalations.

export function determineNextStatus(
  totalAmount: Decimal,
  currentStatus: RequisitionStatus
): RequisitionStatus {
  switch (currentStatus) {
    case "DRAFT":
      return "PENDING_MANAGER";

    case "PENDING_MANAGER":
      if (totalAmount.lte(MANAGER_ONLY_THRESHOLD)) {
        return "APPROVED";
      }
      return "PENDING_FINANCE";

    case "PENDING_FINANCE":
      return "APPROVED";

    default:
      return currentStatus;
  }
}
export function canUserApprove(
  userId: string,
  userPermissions: Permissions | null | undefined,
  userDepartmentId: string | null,
  requisition: RequisitionForAuth
): boolean {
  if (requisition.status === "PENDING_MANAGER") {
    const isDeptManager = requisition.department.managerId === userId;
    const isApproverInDept =
      canApproveManager(userPermissions) &&
      userDepartmentId === requisition.departmentId;
    return isDeptManager || isApproverInDept;
  }

  if (requisition.status === "PENDING_FINANCE") {
    return canApproveFinance(userPermissions);
  }

  return false;
}

Win C — Prisma extensions for notification side effects without blocking writes

Mutations always complete; notification emission runs in a try/catch so observability never breaks transactional integrity.

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;
      },

6. Security & Performance (MSIS Focus)

AreaImplementation
AuthenticationCredentials provider; bcrypt password verification; JWT sessions with 30-day max age; 24h inactivity invalidation in JWT callback.
AuthorizationServer-side auth() + hasPermission / domain helpers in layouts and actions; no Postgres RLS in-repo—security is application-layer (consistent with Prisma + single-tenant DB).
Session integrityEmpty JWT returned after inactivity timeout; session callback returns null when token cleared—forces re-login.
Object storagePresigned URLs for time-limited downloads; server-side uploads with typed keys; avoids long-lived public URLs for sensitive procurement artifacts.
Performance postureServer Components by default; targeted revalidatePath; Prisma logging reduced in production; PDF streaming via @react-pdf/renderer stream response.

7. Agentic Influence

  • Development: AI-assisted tooling (e.g. Cursor) accelerates boilerplate for sprints aligned with project_bible constraints—especially i18n key discipline and schema evolution across procurement modules.
  • Product automation: Prisma notification extension maps model changes to read-permission recipients—a lightweight “event fan-out” without a separate message bus.
  • Document intelligence: PDF PO generation merges locale-specific JSON messages with Prisma-fetched PO data and streams a binary response—suitable for regulated printouts and audit trails.

Key achievements

  • Department RBAC
  • Presigned uploads
  • Audit-friendly workflows

Industry

Enterprise procurement

Stack

Next.js, Prisma, Auth.js, S3, PostgreSQL

Outcomes

Department RBAC, Presigned uploads, Audit-friendly workflows

Project links

Want a broader project snapshot?

Browse the portfolio for shorter project cards with stack, links, and demo references.

View portfolio

More case studies

Hayaat Intake AI — Case Study

Voice-first Medicaid intake, SMS AI assistant, and admin CRM for a Michigan home-help agency.

View case study

COOARD Salon & Barber Booking Platform Case Study

How COOARD engineered a multi-tenant salon operating system with Supabase RLS, Stripe billing, omnichannel notifications, and App Router architecture.

View case study

Twinsting Case Study: Role-Aware Marketplace Architecture

A 2026 engineering case study of Twinsting's Next.js + Node.js architecture for multi-role service commerce, payout orchestration, and realtime delivery.

View case study

Tuttle Case Study: AI Localization Platform Architecture

How Tuttle engineered a multilingual workforce platform using React, Express, Next.js, Stripe, HeyGen, DeepL, and S3.

View case study