Case study · 2026

Five internal-ops jobs, one system.

Timesheets, leave, payroll preparation, IT asset tracking, badge management, and Microsoft 365 activity collection — built as one Next.js 16 application for a Vienna-based international organization, behind Azure AD single sign-on.

Where we started

The HR + payroll system delivered earlier had already replaced the worst of the legacy Power Apps stack. But the rest of the operating layer — timesheets, leave, IT asset tracking, badge management, and Microsoft 365 activity reporting — was still scattered across spreadsheets, inbox threads, and legacy forms only one person could maintain. Payroll preparation continued to cost a person a day per month, audit evidence was still assembled retroactively, and adding another module to Power Apps meant another premium connector or another seat licence to justify.

Three problems compounded:

  • Tribal knowledge. Several critical steps lived in one person’s memory; onboarding a replacement would have meant rebuilding it from scratch.
  • Compounding licensing. Each new feature in Power Apps either required premium connectors or pushed users into seat-license territory.
  • Audit fragility. Reconstructing the chain of approval for any single decision meant cross-referencing email threads, Excel revisions, and Power Apps version history.

What had to be true

Five non-negotiables shaped the build before any code:

  1. Microsoft Entra ID single sign-on must be the default for staff, with a tightly scoped credentials fallback for a single configured super admin.
  2. Session governance must be strict: 20-minute idle timeout, 12-hour absolute timeout, server-tracked sessions in a dedicated table, and client-side warning before expiry.
  3. Rate limiting and account lockout at the API edge, with login attempt tracking and IP/user-agent logging.
  4. Every meaningful action must write an audit row, queryable for compliance without joining inboxes and spreadsheets.
  5. The system must deploy to Azure App Service, integrated with the existing tenant, identity, and monitoring.

Stack and reasoning

  • Next.js 16
  • React 18
  • TypeScript
  • PostgreSQL
  • NextAuth v4
  • Azure AD SSO
  • shadcn/ui
  • Tailwind
  • Radix UI
  • Framer Motion
  • Upstash Redis
  • Azure App Service

The choices, in order of impact:

  • Next.js 16 App Router. Server components for data-heavy dashboards, server actions for mutations, API routes for the integration boundary. One framework, one TypeScript surface, one deploy.
  • NextAuth with Azure AD and credentials providers. SSO for the common case, credentials gated to a single super admin for break-glass access. JWT sessions plus a dedicated app_sessions table for activity tracking.
  • PostgreSQL via the native pg driver, not an ORM. Direct SQL with a small transaction() helper and configurable pool. The data model is relational and queries benefit from being explicit.
  • shadcn/ui on Radix primitives. Component-level control with documented accessibility, without inheriting an opinionated design system that would be hard to break out of.
  • Upstash Redis for rate limiting. Distributed limits across IP and user, configurable per endpoint, no in-process state to coordinate across App Service instances.
  • Azure App Service. The organization’s existing target. No new vendor, no new compliance review.

What was deliberately not picked:

  • Dataverse. Licensing costs and tighter coupling to the legacy environment we were trying to leave.
  • A heavy ORM in the hot path. Native pg with a transaction helper was the right size; ORM magic in the hot path costs more than it saves on this surface.
  • A separate identity provider for staff. Stayed inside Microsoft Entra ID where the organization already had identity governance.

Architecture in plain language

A single Next.js 16 application serves four role-based dashboards (staff, supervisor, HR, admin) and exposes the API routes that own the data model. Postgres holds the canonical state. Sessions live in the app_sessions table with last-activity timestamps, IP, location, and device fingerprint. Rate limits run through Upstash Redis. Email notifications go out via the organization’s Microsoft 365 transport.

Three patterns did most of the work:

  • Role-aware data fetching. Server components and route handlers read the current user’s role and shape the query before sending data to the client. Permission checks live next to data access, not in UI code.
  • Approval as state machine. Timesheet months, leave requests, asset moves, and payroll adjustments all carry a typed status with a transition history. Transitions are guarded server-side, never trusted from the client.
  • Append-only audit log. No record is destructively edited. Every meaningful action emits an audit row keyed by actor, target, action, and timestamp.

The decisions worth defending

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

Microsoft Entra ID SSO as the staff default

Reusing the organization’s identity governance meant no new auth surface for IT to manage, no new password to rotate, and no separate user lifecycle. The credentials path exists, but is gated to a single configured super admin so the SSO discipline is preserved.

Two-tier session timeout, server-tracked

The session lives in two clocks: a 20-minute idle timer and a 12-hour absolute timer. Activity updates the idle timer; nothing resets the absolute one. Both are enforced server-side on every API call, with a client-side warning before the idle timer expires. This is the difference between “our system is convenient” and “our system is auditable.”

Rate limiting and lockout at the API edge

Login attempts are tracked with IP, user agent, and failure reason. Repeated failures trigger temporary lockout. Upstash Redis runs the limits so they are coherent across multiple App Service instances. This kept the platform out of the trap where bot traffic quietly degrades real users.

Append-only audit, not point-in-time edits

Records are not destructively updated. Edits and approvals write new rows; the latest row is the current state. Querying “what did this look like on March 12” becomes a time-bound select. Audit becomes a feature of the schema, not a separate system.

What shipped

  • Role-based dashboards for staff, supervisor, HR, and admin, each with distinct read and write boundaries.
  • Timesheet management with monthly and daily entry, project allocation, supervisor approvals, and HR sign-off.
  • Leave management with multi-level approval and audit trail.
  • Payroll preparation with byte-stable exports for the existing payroll system.
  • IT asset tracking with assignment, condition, handover, and disposal records.
  • Badge and access management with admin oversight.
  • Microsoft 365 activity collection for usage reporting.
  • Audit log covering every approval, edit, role change, and access event.
  • Operational documentation: architecture, tech stack, deployment, setup, design system, development priorities.

What I would do differently

  • Snapshot tests for payroll exports from week one. The payroll boundary is always going to be fragile. End-to-end snapshot tests on export output would have prevented two late-stage debugging sessions.
  • A dedicated audit-query view sooner. The append-only history is 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.
  • Recorded walkthroughs for the roles model. The model is solid, but a 20-minute video would have been worth more to onboarding than the written documentation alone.

Why this kind of work matters

Internal-ops systems become the organization’s actual personnel record, audit trail, and access boundary. Replacing a Power Apps stack module-by-module — without breaking the in-flight payroll cycle, the open audit window, or live access provisioning — is the part of this work that earns its fee. The reward is one place to look when the auditor asks “who changed this on March 12.”

Want a system like this?

I work with Vienna and EU teams on internal ERPs, operations platforms, and Microsoft-365 integrations.

hello@albimeta.com · Response within 1 business day