Case study · 2026

A fragmented HR stack rebuilt as one operating path.

Eight role-based dashboards, multi-level approvals, payroll exports, IT asset tracking, and audit trails — delivered in five months for a 150-person Vienna-based international organization.

Where we started

The organization had grown into a setup most people will recognize: HR data sat in Excel, approvals lived in inbox threads, payroll preparation involved one person reconciling exports by hand, and a legacy Power Apps environment held some of the workflow but could not be extended without specialist help.

Three problems compounded:

  • Tribal knowledge. Several critical steps lived in one person's memory. Onboarding a replacement would have meant rebuilding that knowledge from scratch.
  • Audit fragility. Records existed, but reconstructing the chain of approval for any single decision meant cross-referencing email threads, Excel revisions, and Power Apps version histories.
  • Compounding licensing. Each new feature in Power Apps either required premium connectors or pushed users into seat-license territory. The cost trajectory was not going to stay flat.

What had to be true

Before any code, five non-negotiables shaped the build:

  1. Audit trails must be immutable. Anyone reviewing a year-old approval should see exactly who clicked what and when.
  2. Payroll exports must match the existing payroll system byte-for-byte on the boundary fields. No reformatting, no manual edits.
  3. Eight roles, real ones — staff, manager, HR officer, finance, executive, IT, auditor, admin — each with distinct read and write boundaries.
  4. The team must be able to support and extend the system after handover, with documentation that does not rot.
  5. The system must run from day one in Azure App Service, the existing target environment, integrated with Microsoft Entra identity.

Constraints (3) and (4) shaped everything that followed. Constraint (4) is the one most consultancies skip; we made it a deliverable.

Stack and reasoning

  • Next.js 14
  • React 18
  • PostgreSQL
  • Prisma
  • ShadCN UI
  • Tailwind
  • jsPDF
  • Framer Motion
  • Azure App Service

The choices, in order of impact:

  • Next.js 14 on App Router. Server components for data-heavy dashboards, client components for interactive approval flows, and API routes for the 40+ backend endpoints. One framework, one TypeScript surface, one deploy.
  • PostgreSQL via Prisma. Strong relational integrity for approval chains; Prisma's schema-first migrations made the data model auditable and reviewable as code. No vendor lock-in on data.
  • ShadCN UI on top of Tailwind. Component-level control without inheriting an opinionated design system. Every dashboard, modal, and form was customizable when the workflow demanded it.
  • jsPDF for client-side PDF generation. Timesheets, payroll summaries, and contracts rendered in the browser, signed, and downloaded — no document service to operate.
  • Framer Motion for transitions. Used sparingly, on approval state changes and dashboard loads, where motion communicates state.
  • Azure App Service. Already the organization's target environment for web workloads. No new vendor to onboard, no new compliance review.

What was deliberately not picked:

  • Dataverse. Licensing costs and a tighter coupling to the legacy environment we were trying to leave.
  • A separate auth provider. Stayed inside Microsoft Entra ID where the organization already had identity governance.
  • A heavy state management library. Server components and a small amount of TanStack Query handled the data patterns we needed.

Architecture in plain language

The system runs as a single Next.js application backed by one Postgres database, deployed on Azure App Service. Every approval, edit, and export passes through API routes that write to Postgres with structured audit records.

Three patterns did most of the work:

  • Role-aware data fetching. Server components read the current user's role server-side and shaped the query before sending data to the client. Permission checks lived next to data access, not in UI code.
  • Approval as state machine. Each approvable artifact — timesheet, leave request, asset request, payroll adjustment — had a typed status enum and a history table. Transitions were guarded server-side, never trusted from the client.
  • Append-only history. No record was ever destructively edited. Edits and approvals wrote new rows; the latest row was the current state. Querying "what did this look like on March 12" became a simple time-bound select.

The decisions worth defending

A few choices took the most pushback and turned out to matter most.

Multi-level approval modeled as a graph, not a chain

Real approval flows are not linear. A leave request might need a line manager and HR; a payroll adjustment might need finance, HR, and the executive director. Modeling approvals as a set of independent gates — rather than a strict sequential chain — meant the system could represent every approval the organization actually had, including the ones that surfaced six weeks into the build.

Server-rendered exports, every time

PDF and Excel exports were generated on the server, on demand, from canonical Postgres data — never from a cached client view. Slower by milliseconds, but it meant the export was always the truth, not whatever the user happened to see five minutes earlier. For payroll, this is the difference between a shipped feature and a quiet incident.

One roles table, not hardcoded role checks

Permissions were stored in the database, not compiled into the codebase. Adding a role did not require a deploy. Combined with audit logging, this meant every permission change had a record and a reversal path. The auditor role — one of the eight — was added late in the build and required no architectural change.

Documentation as the last 10 percent, not the first afterthought

The final two weeks were spent writing admin notes, owner roles, support paths, and the runbook for adding a role, retiring a flow, or rolling back a deploy. None of it was glamorous. All of it determined whether the system would still be running cleanly twelve months later.

What shipped

After five months, the organization had:

  • 8 role-based dashboards covering staff, manager, HR officer, finance, executive, IT, auditor, and admin views.
  • 40+ API endpoints across approvals, exports, asset tracking, and notifications.
  • Multi-level approval workflows for leave, expense, asset, and payroll-adjustment requests.
  • A payroll export pipeline producing files the existing payroll system accepted unchanged.
  • IT asset tracking with assignment, condition, handover, and disposal records.
  • Email notifications on state transitions, with quiet hours and digest options.
  • Audit trails for every approval, edit, role change, and access event.
  • Documentation covering admin notes, role inventory, support paths, and a runbook for common operations.

The system replaced a fragmented Power Apps environment, half a dozen Excel files, and an inbox-driven approval process with one operating path.

What I would do differently

A few honest reflections after the system went into daily use:

  • More export tests, sooner. The payroll boundary was always going to be fragile. End-to-end snapshot tests on export output, written in week one, would have prevented two late-stage debugging sessions.
  • A dedicated audit-query view. The append-only history was correct, but querying it across roles for ad-hoc audit questions meant writing custom SQL each time. A small admin UI for "show me the history of X" would have saved time post-launch.
  • Earlier role-permission handover. The roles model was solid, but the team needed a session to internalize it. A 20-minute walkthrough video, recorded once, would have been worth more than the documentation alone.

Why this kind of work matters

Most internal-tools work fails at the boundaries: the moment a workaround becomes the system, the moment the spreadsheet becomes the source of truth, the moment "the person who knows" leaves. This system was an attempt to remove those boundaries — not by adding more tools, but by making one tool the path everyone uses.

Want a system like this?

I work with Vienna and EU teams on internal applications, dashboards, portals, and workflow automation. Email is the best way to start a real project conversation.

mail@albimeta.com · Response within 1 business day