Documentation menu

Access requests

The end-to-end lifecycle of a user-initiated app access request, why approval always goes through a group, and why decisions are one-way.

A user who can see an app but not open it gets a one-click Request access button. This page is the full lifecycle behind that button.

The single most important property: approval never lays a direct per-user grant. It adds the requester to a group, and the reconciler projects the real grant into the IdP. Every grant therefore stays owned by a group, revocable by removing membership and auditable through the group, instead of leaving orphan grants that only a hand-edit in Zitadel can undo.

The lifecycle

StepSurfaceWho
RequestPOST /api/apps/<appId>/request-accessAny signed-in user
Review/admin/access-requests and /admin/access-requests/<id>Admin
ApprovePOST /api/admin/access-requests/<id>/approveAdmin
DenyPOST /api/admin/access-requests/<id>/denyAdmin

The request route authenticates by session only: there is no API-key path onto it. The approve and deny routes are likewise session-gated admin routes.

An optional message from the requester is accepted and capped at 1000 characters.

Requesting is deduplicated

A second call from the same user for the same app, while a request is already PENDING, returns the existing row and reports alreadyPending: true. It does not create a second row and it does not re-send the notification. Admins get one notification per request, not one per click.

On first creation, Atrium emails the app's admin with a Review request CTA deep-linking to /admin/access-requests/<id>. If no email address can be resolved for that admin the request is still recorded; only the notification is skipped.

Users can also file a request through the AI agent or MCP with the user-tier request_app_access tool, which goes through the same service and the same deduplication.

Approving

The admin picks a target group. There is deliberately no "grant directly" option.

Approval modeWhat happens
Existing groupAdd the requester to a group that already grants the app
New groupLast resort. Mint other-<app> (or other-<app>-<roles> when approving with specific roles) and add the requester

The picker is populated with every group that already grants the requested app, each annotated with the role keys it grants, its member count, whether the requester is already a member, and whether the group is directory-managed.

That last flag matters: a group whose roster is mirrored from the directory refuses manual member adds, so it can never be a valid approval target. Offering it would only ever produce a failed approval, which is exactly what used to surface as an unexplained "approval failed" message.

Order of operations

The target group is resolved before the request is marked APPROVED. A bad target, a missing group or a group that does not actually grant the app, leaves the request PENDING so the admin can correct it rather than stranding it in a decided state with nothing provisioned.

Adding the member is the commit point. From there the reconciler projects the Zitadel grant, and it self-heals on later ticks if the immediate write hiccups. That resilience is the whole reason approvals go through groups.

Two identity details worth knowing:

  • The membership row carries the requester's numeric Zitadel id. A legacy directory UUID is canonicalised at approval time, because the reconciler grants against the numeric id and would otherwise 404.
  • The app must have a Zitadel project. Approving an app without one fails explicitly rather than creating a grant that cannot exist.

Re-approving a request for someone who is already a member is a no-op membership plus a reconcile that finds nothing to change.

Denying

Denial records the deciding admin, the timestamp, and an optional reason capped at 1000 characters. The reason is included in the email the requester receives.

Decisions are one-way

Approving or denying a request that is already APPROVED or DENIED is rejected. There is no un-approve and no re-decide: the row is the permanent record of who asked, who decided, when, and why not.

To reverse the effect of an approval, remove the person from the group they were added to. Membership is the source of truth, so the reconciler revokes the grant on the next pass. To reverse a denial, the user files a new request.

Notifications

EventRecipientTrigger
Request createdThe app's adminOnly on first creation, never on a duplicate click
ApprovedThe requesterFires after the membership lands
DeniedThe requesterIncludes the denial reason when one was given

Outcome emails are best-effort: a failure to send does not roll back the membership or the decision. They are transactional in kind, so they are never suppressed by broadcast filtering.

Delivery goes through whichever notifications connector is marked primary.