
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
| Category | Technologies |
|---|---|
| Languages | TypeScript (strict), SQL (via Prisma) |
| Framework | Next.js 15.1+, React 19, App Router (src/app) |
| Auth | Auth.js (next-auth v5 beta), Credentials provider, bcrypt, JWT sessions with Prisma adapter |
| Data | PostgreSQL (Railway), Prisma ORM 6.9 |
| i18n | next-intl 3.x, messages/fr.json + messages/en.json, default locale fr |
| UI | Tailwind CSS 4, Radix UI (shadcn-style), Lucide, next-themes, Recharts |
| Forms & validation | react-hook-form, Zod, @hookform/resolvers |
| Client state | Zustand (persisted procurement mode + display currency, cookie sync) |
| Documents | AWS 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]/…withnext-intlmiddleware for locale prefixing (frdefault,ensecondary). 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
permissionsaligned with databaseRole.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)
| Area | Implementation |
|---|---|
| Authentication | Credentials provider; bcrypt password verification; JWT sessions with 30-day max age; 24h inactivity invalidation in JWT callback. |
| Authorization | Server-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 integrity | Empty JWT returned after inactivity timeout; session callback returns null when token cleared—forces re-login. |
| Object storage | Presigned URLs for time-limited downloads; server-side uploads with typed keys; avoids long-lived public URLs for sensitive procurement artifacts. |
| Performance posture | Server 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_bibleconstraints—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