Case study · 2026

Training evaluation and an alumni network, in one codebase.

Admissions, training management, participant evaluation, analytics, alumni profiles, jobs, events, forum, and messaging — four roles, four sign-in flows, one operating layer on Next.js 15 with Microsoft Entra ID and SharePoint.

Where we started

The organization needed two products that kept being treated as one: a training evaluation platform for staff, project officers, and participants — and an alumni network for profiles, jobs, events, forum, resources, and messaging. They shared users at the edges, they shared an identity tenant, and the team did not have the appetite to run them as two deployments with two deploy pipelines and two on-call rotations.

Three forces drove the rebuild:

  • Two products, one user lifecycle. A participant today is an alumnus tomorrow. Two systems meant duplicate accounts and stale data.
  • Staff already lived in Microsoft Entra ID. Adding a separate credentials surface for staff would create a second password lifecycle to govern.
  • External users needed structured invitation, not self-registration. Admins create accounts; users complete setup. No drive-by sign-ups.

What had to be true

Six non-negotiables shaped the build before any code:

  1. Two products in one codebase, each with its own route tree, its own admin surface, and a shared identity model.
  2. Four roles with distinct access: ADMIN, PROJECT_OFFICER, PARTICIPANT, ALUMNI.
  3. Three sign-in surfaces: Entra SSO for staff, email/password for participants, email/password for alumni — with admins blocked from credentials login except a single configured super admin.
  4. MFA available for password users, with authenticator-app setup, trusted-device support, and a disable flow.
  5. SharePoint is the primary file storage target; Azure Blob Storage is deprecated and kept only for backward compatibility.
  6. Microsoft Graph / Office 365 SMTP OAuth2 is the email transport for invitations, reminders, and notifications.

Stack and reasoning

  • Next.js 15
  • React 19
  • TypeScript
  • Prisma
  • PostgreSQL
  • NextAuth v5
  • Microsoft Entra ID
  • Microsoft Graph
  • SharePoint
  • Tailwind
  • Zod
  • Playwright
  • Vitest

The choices, in order of impact:

  • Next.js 15 App Router. Two route trees (/admin, /project-officer, /participant, /alumni/*) with role gates at the route boundary. Server components for read-heavy pages, server actions for mutations.
  • NextAuth v5 with multiple providers. Entra ID for staff, credentials for participants and alumni, MFA for credentials, account lockout, audit logging.
  • Prisma + PostgreSQL. Strong relational integrity for users, roles, trainings, questionnaires, alumni profiles, jobs, events, and forum threads. Reviewable migrations in PR.
  • SharePoint via Microsoft Graph. Files live where the organization already governs file lifecycle. Azure Blob marked deprecated.
  • Microsoft Graph for email. OAuth2-authenticated transport from noreply@; no app passwords, no SMTP password rotation.
  • Playwright + Vitest. End-to-end coverage for the auth and role-gate paths; unit coverage for the rest.

What was deliberately not picked:

  • Self-registration. No public signup. Admins create accounts, users complete setup via invitation email.
  • A separate auth provider for participants and alumni. NextAuth credentials with MFA was the right size; one auth surface to govern.
  • Two separate deployments for the two products. Shared identity, shared user lifecycle, and one deploy pipeline outweighed the “cleaner separation” argument.

Architecture in plain language

A single Next.js 15 application serves four role-based route trees on top of a shared Postgres schema. NextAuth v5 handles three sign-in flows: Entra SSO for staff, credentials for participants, credentials for alumni. Files upload to SharePoint via Microsoft Graph. Email sends through Microsoft Graph using a service principal. Rate limits and audit logs back the security model.

Four patterns did most of the work:

  • Role gates at the route boundary. Each top-level route tree has middleware that enforces the role required to enter. No conditional rendering inside shared pages.
  • Invitation-based user lifecycle. Admins create users; the platform sends a setup-password invitation; users complete setup; lockout and MFA happen from there. Self-registration does not exist.
  • Admin impersonation with audit. Admins can impersonate a user for support purposes; the impersonation token is HMAC-signed, time-limited, and every action is audited.
  • Field-level privacy on alumni profiles. Alumni control which fields are visible to other alumni, to project officers, or to nobody. Privacy is enforced at the API boundary, not in the UI.

The decisions worth defending

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

Two products in one codebase

The instinct is to split: one repo for evaluation, one for alumni, two deploy pipelines. Reality: the user lifecycle crosses both products, the identity tenant is one, and the team is one. One codebase is the cheaper long-term operation. The route-tree separation handles the boundary inside the code.

Microsoft Entra SSO for staff, credentials for externals

Trying to put external participants and alumni through Entra ID would have created a separate guest-account governance burden the organization did not want. Staff stay in Entra; externals get credentials with MFA, account lockout, and trusted-device support.

SharePoint as primary file storage

The organization already governs file lifecycle in SharePoint. Putting files in Blob Storage would have created a parallel file-retention problem with different audit tooling. SharePoint via Microsoft Graph is the right boundary; Blob is deprecated and kept only for backward compatibility.

No self-registration, ever

Every account is created by an admin and completed via invitation. This eliminates drive-by signups, makes the user model auditable end-to-end, and keeps account lockout meaningful instead of becoming a CAPTCHA arms race.

Field-level privacy on alumni profiles

Alumni get to decide which fields are visible to which audience. The platform enforces this at the API, not in the UI — so a careless client implementation cannot leak a private field. Privacy is a query concern, not a rendering concern.

What shipped

  • Four role-based route trees with middleware-enforced access control.
  • Three sign-in surfaces: Entra SSO at /staff/signin, credentials at /auth/signin and /alumni/signin.
  • MFA setup, confirmation, verification, disable, and trusted-device support for credentials users.
  • Admin user management, MFA reset, and audited impersonation.
  • Training management with lecturers, participants, and questionnaires.
  • Participant questionnaire response flows with evaluation analytics and export.
  • Alumni profiles with field-level privacy controls.
  • Alumni directory, map, jobs, events, resources, forum, feature suggestions, and messaging.
  • Moderation and admin oversight tools for the alumni network.
  • Rate limiting, account lockout, CSRF protection, audit logging, and soft-delete across the security surface.
  • SharePoint file uploads via Microsoft Graph; Azure Blob kept only for backward compatibility.
  • Microsoft Graph email transport for invitations, reminders, and notifications.

What I would do differently

  • Treat the alumni network as a separate top-level app inside the monorepo from week one. The shared codebase is correct, but a clearer in-repo boundary between the two products would have shortened decision time on shared-vs-product-specific changes.
  • Earlier end-to-end coverage on the invitation flow. The invitation lifecycle is the most failure-prone surface (email transport + token validity + setup-password race). More Playwright coverage on that path would have caught two production incidents at PR time.
  • Stronger field-level audit on alumni profile changes. Privacy is correct; the audit trail on which fields changed when is less granular than it should be.

Why this kind of work matters

Multi-product platforms inside one organization tend to either fragment into two deployments that diverge or collapse into one app that nobody can reason about. A clean role model at the route boundary, one user lifecycle across both products, and one auth surface to govern is the cheapest way to keep the system both extensible and auditable.

Want a system like this?

I work with Vienna and EU teams on multi-product platforms, training and evaluation systems, and Microsoft-365-integrated internal applications.

hello@albimeta.com · Response within 1 business day