OOOlorede.
Back to Projects
Full-StackFeaturedSeptember 2026

PES

Universities and companies ran staff appraisals, stress surveys, and staffing math on spreadsheets that nobody trusted after the fact. PES turns those into one system where a score can be traced back to the person who entered it.

Next.js 14 (App Router)React 18TypeScriptPostgreSQLPrisma 5Redux ToolkitFormik / react-hook-formZodTailwind CSS 4Radix UI / shadcn-style component kitJWT (jsonwebtoken) + bcryptPaystackPayPal Subscriptions APIStripe (present, inactive billing path)CloudinaryNodemailer / ResendDocker ComposeVercelNeon
PES
Overview

A university department head or company HR admin who wants to know whether a team is overstaffed, understaffed, or burning out has historically had to run separate spreadsheets for appraisal scores, stress surveys, and staffing formulas, then reconcile them by hand before anyone signs off. PES puts appraisal, performance, stress evaluation, and workforce-sizing models (queueing-theory personnel utilization, ANOVA-based stress analysis, staff-number estimation) behind one role-gated workflow: a staff member enters a score, their supervisor enters a counter-score for the same period, and an admin accepts or rejects the pair, which either averages them into a final record or sends both back for rework. The models themselves are the part a generic HR tool can't fake: they encode named equations (K*/H* queueing outputs, ANOVA F-statistics with a documented reset rule, load-classification tables) with inline guidance so a non-statistician can run them and still trust the number. It's built multi-tenant so one Postgres database serves many organizations at once, each seeing only its own roster and results, with subscription tiers (Paystack and PayPal) gating which models an organization can run.

Architecture

Next.js App Router serves both the UI and roughly 200 API route handlers. Identity is established per-request through a verified JWT claim, and the organization a query is scoped to comes only from that verified token, never from the request body or URL. That's a rule the codebase enforces with a documented grep audit, because it had been broken repeatedly by routes that looked correctly scoped but weren't. Most of the ~40 evaluation, appraisal, and recognition tables key their tenant by a plain-text organization name string rather than a foreign key, documented as the largest structural debt in the schema and slated for an org_id backfill before any real customer onboards. The Prisma schema declares 62 models but only 11 have real migration files (the rest were pushed directly to the database), so new schema changes are hand-written and applied with db execute plus migrate resolve --applied until the history is baselined. Billing runs two parallel schemes for historical reasons: a flat Paystack-shaped table that signup actually writes to, and a relational PayPal-shaped set the UI doesn't call yet, with a single catalog file now the intended source of truth for prices and entitlements.

Technical Decisions
01

Authorization identity source

Chose

Every API route resolves the caller's organization from a server-verified JWT claim, never from the request body or a URL parameter.

Why

An organization's appraisal and stress data is confidential inside a university or company. A rule that looked right but let the org come from something the caller controlled would let anyone read or write another tenant's staff records by editing a request. A prior audit found exactly that pattern across a dozen-plus routes, including three 'fixed' in a commit titled to prevent that leakage and still leaking afterward.

Trade-off

Every new route needs a few extra lines of guard code and a mental check AGENTS.md now spells out explicitly. Accepted because the cost of getting it wrong is a cross-tenant data leak, not a slow API.

02

Counter-score reconciliation instead of single-entry scoring

Chose

Every appraisal and performance score is captured twice: staff, then supervisor as a counter-score, and an admin explicitly accepts (averaging) or rejects (sending both back).

Why

A single self-reported or single supervisor-reported score is easy to dispute after the fact. Two independent entries plus an explicit admin adjudication step means a contested score has a paper trail: who entered what, who signed off.

Trade-off

Roughly doubles data-entry burden and adds a review queue. Acceptable where evaluation outcomes affect pay or continued employment and need to survive a challenge.

03

Multi-tenancy keyed by organization name string, not a foreign key

Chose

Roughly 37 tables carry an org column as free text rather than an org_id foreign key.

Why

This shipped in the original codebase before the current maintainers took over. Rewriting all 37 tables' join keys mid-build would have blocked every other fix behind a schema migration touching the entire query surface. Shipping security and correctness fixes on the existing shape first got real bugs closed faster.

Trade-off

Until the migration runs, an organization can't be renamed without orphaning rows, and a typo'd org name silently creates unjoinable data. Documented as a pre-launch blocker, scheduled before any real organization's data is at stake.

04

Two billing schemes running in parallel

Chose

Kept the Paystack-shaped table signup actually writes and the PayPal-shaped relational set, while consolidating pricing and entitlement logic into one catalog file.

Why

Four different places in the codebase quoted different prices for the same plan before this was cleaned up, and signup wrote status: 'success' without checking money had moved. Centralizing on one catalog closes the disagreement.

Trade-off

The two schemes still coexist and only one is wired to the live signup path, leaving the other as dead weight until someone migrates or drops it. Left as an open decision, per the project's own documentation.

Future Improvements

Backfill the 37 name-keyed tables onto a real org_id foreign key so renaming an organization can't silently orphan its own records.

Wire the plan catalog's entitlement check into the actual route guards; a basic-tier user who knows a model's URL can currently reach it anyway.

Fill the three still-missing catalog prices and flip billing enforcement on, since signup can currently complete without a real payment reference while the client finishes testing.

Extend Zod validation past the 18 routes that currently have it (of roughly 205), so malformed requests fail with a field error instead of a raw database exception.