All work

04 / Identity & access

User management — sign-in, roles and approvals for an enterprise portal

Thirteen or more flows, owned end to end, for the least glamorous and least forgiving part of an enterprise product: who gets in, what they may touch, and who has to agree before any of it changes.

Role
Sole designer
Partner
1 product owner
Timeline
2023–2025, ~18 months
Users
Merchant admins and their users
13+ flows Each scoped from a real customer pain point, not a backlog
3 fields → 2 Fewer steps to sign in, and stronger authentication
Nothing unilateral Access changes require a second person to approve
A laptop showing the user management work: the Visa Acceptance Solutions sign-in screen
            and a create-new-password step with its requirements listed, and behind them the role
            permissions screen and the account recovery settings.
Sign-in, account recovery and role-based access — thirteen-plus flows across the platform, owned end to end.

This work is confidential. Screens come from my own design files and all data shown is placeholder data.

Context

Every enterprise product has a user management section, and almost nobody wants to design it. It is forms, tables, permissions and confirmation dialogs. It has no hero screen. But it is also the part where a mistake does not just annoy someone — it either locks a business out of its own money, or lets the wrong person reach it.

The portal made this harder than the usual case. A single person can belong to more than one organization. Administrators delegate to other administrators. Some changes are so sensitive that no one person should be able to make them alone. So "user management" here is not a settings page; it is a small identity and access product with its own rules.

How the work ran

I owned this area for about eighteen months as the only designer, working with one product owner. It never arrived as a project brief. Each flow started from a specific customer complaint or support pattern — someone locked out, an admin unable to grant what they needed, a change nobody could trace — and we scoped it from there. Thirteen or more flows accumulated that way.

Because the same product owner kept returning with the next one, I could design each flow knowing the ones around it. That is why the states are consistent across all of them rather than each screen inventing its own version of a confirmation, an error, or a success message. It also meant the work was continuous in a different sense: designs went out, engineering built them, real behavior came back, and the next flow started from what we had learned.

Sign-in: three fields to two

The original sign-in asked for organization ID, username and password, then offered one button. The redesign asks for organization ID and username, then offers Log in with Passkey as the primary action and Continue with Password as the fallback.

Two sign-in screens side by side. The before version asks for organization ID, username
              and password, with a single log-in button. The after version asks only for organization ID and
              username, then offers Log in with Passkey as the primary action and Continue with Password as
              the secondary one.
The same sign-in, before and after. Removing the password field made the form shorter and the default authentication stronger.

The reason this works is not that a field was deleted. It is that the first step now answers a single question — who are you, and where — and the choice of credential comes after. Once the password is no longer assumed, a passkey can be the default rather than a feature buried in settings, and most people never type a password again.

That is the rare case where the friction and the security argument point the same way. Fewer steps for the person signing in, and a credential that cannot be phished, reused, or read off a sticky note. I did not have to trade one against the other, which is worth saying because in this domain you usually do.

The sign-in screen in an error state, annotated on the design: when a user leaves the
              fields empty and clicks Log in with Passkey, a banner reads that login was denied because
              fields below are missing, and each empty field is marked with its own inline error.
The empty-field case, annotated on the design. A summary at the top for the person who scanned, and an inline error on each field for the person who needs to know which one.

Two things about that error state I would defend anywhere. It says what was wrong and marks the specific fields, because a banner alone makes people hunt. And it never reveals whether an organization ID or username actually exists — a sign-in form that is helpful about which half you got right is a sign-in form that helps an attacker enumerate accounts.

Passkeys, including the unglamorous half

Offering a passkey at sign-in only works if creating one is obvious and removing one is possible. I designed both.

The passkey creation flow mapped out: the user profile screen, the prompt to add a
              passkey, the operating system's own biometric dialog, and the confirmation states that
              follow, with branches for success and for the user cancelling.
Creating a passkey, mapped with every screen it touches — including the point where the browser takes over and the design has to hand off gracefully.

The interesting constraint is that the middle of this flow is not mine. The biometric prompt belongs to the operating system, it looks different on every platform, and it can fail or be dismissed in ways I cannot style. So the design work is the handoff: setting expectations before control leaves the page, and handling every way it can come back — created, cancelled, unsupported device, or timed out.

Deletion mattered just as much. A credential you cannot remove is a liability on a lost laptop, so a passkey has to be revocable from the profile with a clear consequence stated before it goes.

Changing a phone number, without creating a dead end

Recovery details are a quiet security boundary. If someone can change the phone number that receives one-time codes, they can take over an account. So the change has to be verified — and the verification has to not strand people.

Four steps of changing a phone number. First the edit dialog showing the current
              number and asking for the new one twice plus the account password. Second a verification
              dialog for the one-time passcode sent to the new number. Third the same dialog after
              requesting a new code, showing a sent confirmation. Fourth the profile page with a green
              success message confirming the number is verified.
Edit, verify, resend, confirmed. The resend state exists because the most common failure in a one-time-code flow is a code that never arrives.

The step I care about most is the third one. A one-time code flow that only handles the happy path fails constantly in reality — codes are delayed, go to an old handset, or arrive after they expire. Without a visible way to request another, and confirmation that the new one was sent, the user's only remaining move is to call support. Designing the resend path is the difference between a flow that works and a flow that generates tickets.

The dialog also asks for the account password before the change, and confirms the new number twice. Both are friction I argued for rather than against, because the thing being protected is the mechanism that protects everything else.

Roles and permissions: the same screen, different powers

Permissions are usually treated as a data problem. They are also an interface problem: two people open the same page and must correctly understand two different sets of possibilities.

The roles list shown twice. In the view-only version the table has no action controls
              and no create button. In the manage version the same table gains row-level edit and delete
              controls, selection checkboxes, and a create role action.
The roles list with and without management rights. Actions the user cannot take are absent rather than present-but-disabled.

The decision here was to remove actions a user cannot perform rather than show them disabled. Disabled controls are defensible when the user could plausibly enable them — fill in a field, select a row. But a permission is not something you can satisfy by trying harder, so a greyed-out Delete button is only an advertisement for access you do not have. It invites a support ticket and tells you something about the account model you did not need to know.

Maker–checker: nothing important happens alone

For the most sensitive changes, one person is not allowed to act unilaterally. One user proposes; a different user approves. The pattern is standard in banking, and the design problem is that a request now has a life of its own — it can be pending, approved, rejected, or executed and failed, and the two people involved need to know which without asking.

Maker Checker Done Send Approve Reject → notify maker notify checker
The maker–checker loop, redrawn from my original diagram. Every arrow is also a notification, which is where most of the design work went.

Drawn as a diagram it looks trivial. The work was in the states around it: what the maker sees while waiting, what the checker sees when a request arrives, what happens when a request is approved but then fails on execution, and how each party finds out. A rejection that does not explain itself is worse than no workflow at all, because the maker simply submits it again.

One username, many organizations

The subtlest problem in this area had no screen at all. The same username can exist in more than one organization. Sometimes that is genuinely one human being with access to several businesses; sometimes it is two different people who happened to pick the same name.

A decision diagram comparing two cases. On the left, the same user across multiple
              organizations with the same email and username, resolving to a single notification email. On
              the right, different users across organizations with different emails but the same username,
              resolving to separate emails sent individually.
Working out when two accounts are the same person. The email address, not the username, turned out to be the thing that decides it.

It matters because of notifications. Send one email when it should have been two and somebody learns about an account that is not theirs. Send two when it should have been one and a legitimate user gets duplicates and starts ignoring them — which is worse, because these are the security emails.

The resolution was to treat the email address as the identity and the username as merely a label. Same email across organizations means one person and one message; different emails mean different people and separate messages, even when the username matches. Writing that rule down was more valuable than any screen in this case study, because it removed a question that would otherwise have been answered differently in every flow.

What I'd do differently

I would have pushed for a written model of the permission system at the start rather than deriving it flow by flow. By the tenth flow I understood the rules well; I would have designed the earlier ones better with that understanding, and some of my early screens were more consistent by revision than by intent.

What I would keep is the sourcing. Every flow here began with an actual person who could not do something, which is why none of it is speculative and why the product owner kept coming back. That is not a design method anyone teaches, but for a part of the product with no hero screen and no obvious roadmap, following the complaints was the most reliable way to find work worth doing.