Projects · Entra ID

End-to-End IAM, Part 1: The Architecture Behind Elytra and Ascendra

By Andreas Krisby 2026-07-26 4 min read

This is part 1 of a series that goes deeper into end-to-end-iam, a side project where I built a fictional HR and IGA landscape connected to a real Entra ID tenant. The original writeup covered the big picture. This part zooms in on a single decision: why Elytra and Ascendra are fully separate systems, and what that actually costs and buys in practice.

Two systems, one boundary

Elytra is the HR system. It’s the source of truth for employment data: when someone is hired, changes role, or leaves, Elytra is the one that decides what’s true. Ascendra is the IGA platform (Identity Governance & Administration). It owns identities, roles, provisioning, SSO connections, and audit logs.

In a lot of real organizations, that boundary blurs over time. The HR system gets an integration that writes straight into the directory “because it’s simpler,” or the IGA platform gets read access to the HR database “just to avoid waiting for an event.” Each of those shortcuts feels reasonable in the moment, but they make it harder to know who actually owns a decision, and harder to replace either system later.

So I set three hard rules for the project:

  • Separate Convex deployments for Elytra and Ascendra
  • No shared database
  • No direct communication between the systems

Convex is a backend platform that bundles a database, server functions, and real-time updates into one thing. Running Elytra and Ascendra as two fully separate Convex deployments means they’re not just logically separated in code — they actually run as two independent backends with no way to accidentally read each other’s data.

The only connection: an append-only outbox

If the systems can’t share a database or call each other directly, how does Ascendra even find out that someone was hired or has left?

The answer is an outbox — a log that Elytra only ever adds events to, never edits or removes from (append-only). Elytra publishes four kinds of events to it:

  • employee.created
  • employee.updated
  • employee.activated
  • employee.terminated

Ascendra never reads Elytra’s database. It only reads this log, regularly (it “polls” it), and keeps track of where it last left off using a cursor — a marker pointing at the last event it has already fetched. The next time Ascendra reads the log, it only pulls what’s been added since then. From these events, Ascendra builds up its own picture of every identity, entirely independent of how Elytra stores the same information internally.

This pattern is often called event-driven integration or pub/sub (publish/subscribe): one side publishes facts about what happened, the other side subscribes to and interprets them on its own terms. Neither side needs to know how the other is built internally.

Why not just an API call?

The simplest alternative would have been to let Elytra call Ascendra directly whenever something changes — a plain synchronous API call. I chose against that for a few reasons:

  • Loose coupling. If Elytra has to know how to talk to Ascendra’s API, the systems are no longer independent. A schema change on Ascendra’s side risks breaking Elytra too.
  • Resilience. If Ascendra is down when an employment record is created in Elytra, that shouldn’t matter. The event just sits in the outbox until Ascendra is back and can read it.
  • A single truth about ordering. Because the outbox is append-only and read in order using a cursor, Ascendra can always trust that it sees events in the same order they actually happened, even if it was offline for a while.

The price of this is eventual consistency: there’s a small window where Elytra knows something Ascendra hasn’t read yet. In an HR/IGA scenario, that’s a reasonable trade-off. A few seconds or minutes of delay before a new account shows up in Entra ID is rarely critical. Never risking that a terminated employment “disappears” between systems matters much more — and that’s what the periodic reconciliation covered in part 3 of this series is there to guarantee.

What the architecture actually buys

This split mirrors how many larger organizations already separate HR from their IGA platform: HR owns the truth about who is employed, the IGA platform owns what employees actually get access to. Building it myself, in miniature, made it clear why the pattern exists: it lets each system be replaced or scaled independently, it makes debugging easier because every event is a traceable fact in a log, and it forces a clear ownership boundary that otherwise gets blurred easily in a monolith.

The next part in the series covers what happens on the other side of that boundary: how Ascendra takes these events and actually provisions real accounts in Entra ID via Microsoft Graph.

Elytra – Onboard employee, the first step of the JML flow