A UI that prevents mistakes before the backend has to refuse them
A good backend refuses invalid actions with clear errors. A good admin panel makes sure the admin rarely meets them. Here is how the auth admin panel guides setup in the order the backend enforces, warns about broken configurations hours before a real user hits them, stays honest about what it can't know, and ships the integration docs inside the product.
The problem
Admins discovered misconfigurations through backend errors or, worse, through users who couldn't log in.
The result
Setup follows a checklist, broken states are flagged where they're visible, the UI never claims what it can't verify, and integration guides live next to the settings they explain.
Stack
The problem
Our central auth service has strict rules, and that's good. It refuses to activate a group for a disabled app. It refuses to tick permissions that aren't in the catalog. It refuses to remove the last Root Admin. Every refusal comes with a clear message.
But a refusal is still a failure the admin had to walk into. And some mistakes don't produce any refusal at all. They produce a configuration that is technically valid and practically broken:
- an application registered, but its permission catalog never synced, so there's nothing to tick,
- an application with no group activated, so nobody can enter it,
- a user with no group, so they can't log in to anything,
- a group activated for an app with zero permissions, so members can enter, but every endpoint answers 403.
None of those are errors from the backend's point of view. Users discover them the next morning, as "I can't log in" tickets.
The admin panel's job is to make those states visible before a real user hits them.
The details
The order the backend enforces
Connecting a new application involves steps that must happen in order:
- Register the application (and save its API key).
- Sync its permission catalog from the app's manifest endpoint.
- Give at least one group access to it.
- Tick permissions for every group that has access.
- Make sure users are in those groups.
Skip step 2, and step 4 has nothing to show. Skip step 3 or 5, and nobody can log in.
Valid but broken
The dangerous states are the ones that pass every validation. They need to be detected by reading the data the panel already has, and shown where the admin will see them.
The challenges
- Warnings must be specific. "Something is misconfigured" is noise. "Nobody can enter this app because no group is activated" is actionable.
- Warnings must be in context. On the app card, on the user row, on the permission screen: wherever the admin is when it matters.
- Warnings must not lie. Advice that can't be acted on is worse than silence.
- The UI must be honest about what it can't know. Some facts live in other systems.
- Developers connecting an app need instructions. Docs in a separate wiki drift away from the actual settings.
The solution
1. A setup checklist in the backend's order
Each application's overview page starts with a checklist that mirrors the enforced order:
Follow the checklist in order. The backend enforces this order, and skipping a step blocks login for real users.
✔ Application registered ✘ Permission catalog synced → [ Sync catalog ] ✘ At least one group can enter → [ Give a group access ] ⚠ Every group here has permissions → [ Fix ] ✘ At least one user in those groups → [ Assign ]
Each step is computed from data the panel already loaded (permission count, group count, groups with no permissions, member count), and each unfinished step links straight to the screen that completes it. The "every group has permissions" step is a warning (amber), not a blocker: a group with no permissions is legal, but rarely intended. A new app goes from "registered" to "usable" by following the list, without reading a manual.
2. Warnings where the problem is visible
Instead of a single "health" page, warnings appear on the object that has the problem, as amber badges with a reason:
| Where | Warning | Reason shown |
|---|---|---|
| App card | Catalog not synced | "Nothing works until the permission catalog is synced." |
| App card | No group activated | Nobody can enter this application yet. |
| User row | No group | "This user cannot log in to any application." |
| Permission screen | empty catalog | "billing has not been synced. Pull its permission catalog first, then come back." |
The user-row warning is the one I like most. Without it, "user has no group" looks like a perfectly normal empty cell. With it, an admin scanning the user list immediately sees who will be locked out.
3. Explain the subtle states at the moment they're created
Some states are only dangerous in combination. Removing every permission from a group in an app is valid, but its effect isn't obvious, so the save triggers an explanation:
This leaves "Finance" with no permission in billing. Its 42 members can still enter billing (the access is still there) but every endpoint answers 403. To shut them out completely, remove the group from billing instead.
The admin learns the difference between "no permissions" and "no access" at the moment it matters, and is pointed to the right tool if that wasn't their intent.
4. Don't give advice nobody can act on
When an application is disabled, every fix a setup warning would suggest (sync, activate, tick) is refused by the backend with a 409 until the app is restored. Showing those warnings would send the admin into a series of errors.
So a disabled app shows a different message instead:
Nobody can enter. Nothing was deleted: restoring brings all of this back as it is.
It's one sentence that answers the two questions an admin has at that point: what's the effect? and did I lose anything?
5. Be honest about what the panel can't know
The panel can push the list of allowed users to applications that keep a local copy (each push carries a version number). But the auth service doesn't record whether the app accepted a push.
A typical UI would show a green "Sync: working ✓". That would be a lie. So the screen shows only what it actually knows, and says what it doesn't:
Version: 42. +1 on every send. The app's own copy should show the same number. If it is behind, a send was refused.
ℹ The auth service does not record whether the app accepted a send. Check the app's user copy, or see Troubleshooting in the setup guide.
The admin gets a concrete way to verify (compare version numbers) and a "Send the list again" button, instead of false reassurance.
The same honesty applies elsewhere. The README of the panel even keeps a table of backend gaps the UI works around, for example:
| Gap | What the UI does |
|---|---|
| no "get one user" endpoint | the detail sheet uses the row already loaded; deep links fall back to a wider fetch |
| no search on the user list | client-side filter over the loaded page |
| no API key rotation | the reveal-once dialog makes saving the key unavoidable |
| no "last synced at" field | the catalog count is shown instead of a fake timestamp |
Writing these down keeps workarounds deliberate, and gives the backend team a ready-made to-do list.
6. Bulk import that never fails as a whole
Users are imported from the company ERP. The backend creates users one at a time, so the import runs a loop. One duplicate must never fail the batch:
async function run(users: { name: string; email: string }[]) {
setProgress({ done: 0, total: users.length });
const failures: { name: string; reason: string }[] = [];
let added = 0;
for (const [index, user] of users.entries()) {
try {
await createUser(user);
added += 1;
} catch (error) {
failures.push({ name: user.name, reason: apiError(error) });
}
setProgress({ done: index + 1, total: users.length });
}
await queryClient.invalidateQueries({ queryKey: queryKeys.usersRoot });
setProgress(null);
setSummary({ added, failures });
}
The admin sees live progress ("12 / 30"), and at the end a summary: "28 added. 2 failed: …" with the backend's reason for each. Nothing is half-reported, and nothing stops at the first problem.
7. Docs inside the product, from one file
Connecting a backend to the auth service needs code: a guard, a few constants, a manifest endpoint. Those instructions used to live in documents that drifted away from reality.
Now the integration guides (one for NestJS, one for Flask) are plain Markdown files shipped with the panel. The panel renders them on a page next to the settings they explain, and offers the same file for download:
export const GUIDES = [
{ id: 'nestjs', label: 'NestJS', description: 'TypeScript, same stack as our own services.', file: 'integration-nestjs.md' },
{ id: 'flask', label: 'Flask', description: 'Python, with PyJWT, redis and requests.', file: 'integration-flask.md' },
] as const;
- One text, never two. The page and the download are the same file, so they can't disagree.
- Developers can read the guide offline, next to their code.
- Rendering uses a tiny Markdown parser for the small, fixed subset the guides use (headings, lists, tables, code fences), with a comment saying to switch to a full library if the guides ever need more. That kept the Docker build free of an extra dependency for a handful of files.
Lessons learned
- Valid isn't the same as working. Detect states that pass validation but break the product.
- Mirror the enforced order in the UI. A checklist turns backend rules into a guided path.
- Warn on the object, in context. An amber badge on the user row beats a separate health page.
- Explain consequences when they're created. That's when the admin can still change their mind.
- Silence useless advice. Don't suggest fixes the backend will refuse.
- Never claim what you can't verify. Show what you know, say what you don't, and give a way to check.
- Write down your workarounds. A table of backend gaps keeps the UI honest and the roadmap clear.
- One failure shouldn't sink a batch. Collect failures, finish the loop, report everything.
- Ship docs with the product. One Markdown file, rendered and downloadable, can't drift.
FAQ
How can an admin UI prevent configuration errors?
Guide setup in the order the system requires, detect states that are valid but broken, show specific warnings on the objects that have the problem, and explain consequences at the moment an admin makes a change.
Should the UI show a "working" status for integrations it can't verify?
No. Show what you actually know (for example a version number or last attempt), say clearly what you can't confirm, and give the admin a way to check or retry.
How should bulk imports handle individual failures?
Process every item, collect failures with their reasons, show progress while running, and present a summary at the end. One bad row shouldn't stop or roll back the rest unless the data truly requires all-or-nothing.
Where should integration documentation live?
Close to the product it describes. Shipping guides as files with the app, rendered in the UI and downloadable from the same source, keeps them in sync with the actual settings.
Facing a similar problem?
I help teams fix performance, backend and reliability issues in production.
Let's talk