# How do I manage user roles across multiple apps?

> Keep roles in one central place, usually your identity provider, and have every app read them from there instead of keeping its own copy. Name permissions per app, like billing:invoices:refund, deliver them in tokens at sign-in, re-check them through an API, and offboard people by blocking their account once. In loginos, one platform’s roles apply on all of its domains.

Your product has grown into several apps: the main app, an admin console, a help center, maybe a shop on its own domain. Each one started with its own `is_admin` column. Now giving a support agent access means editing three databases, and when someone leaves, one of them gets forgotten.

The fix is a decision about architecture. One place decides who has which role, every app reads from there, and each app checks permissions in its own code.

## Should roles live in one place or in each app?

There are three ways to split the work between a central store and your apps:

| Model | Where roles live | Strength | Weakness |
| --- | --- | --- | --- |
| Per app | Each app’s own database | Each team changes roles without asking anyone | Roles drift apart, and offboarding misses an app |
| Central | Your identity provider or an authorization service | One place to grant, revoke and review | Every app depends on one shared model |
| Hybrid | Central for global roles, each app for rights on single objects | Each kind of right lives where it is checked | Two places to look when you debug access |

Most teams end up with the hybrid. Global facts belong in the central store: who is staff, who is an editor, who may refund orders. Facts about single objects stay in the app that owns them: who owns project 7, who belongs to workspace 12. Those change all day and matter to one app only.

A central store also answers the audit question “who can refund orders?” in one query. With per-app roles, you ask every database and hope they agree.

## How should you name roles and permissions?

Role-based access control (RBAC) gives people roles and gives roles permissions. NIST’s model of it became the ANSI/INCITS 359 standard in 2004. In RBAC, your code checks permissions, the small and stable part, and roles stay with the people who manage access.

With several apps, put the app’s name in each permission key, in an `app:resource:action` pattern:

| Role | Permissions |
| --- | --- |
| `member` (the default) | `blog:comments:write`, `shop:orders:create` |
| `editor` | `blog:posts:write`, `blog:posts:publish` |
| `support-agent` | `admin:users:read`, `shop:orders:read`, `shop:orders:refund` |

The prefix shows which app owns a permission, and two apps can both have `posts` without a clash. A role can combine permissions from several apps, because roles describe jobs, and jobs cross app boundaries.

Watch for role explosion, which the [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html) describes as defining too many roles. If you find yourself creating `editor-workspace-12`, that is per-tenant data. It belongs in the app, not in the global role list.

## How do roles reach each app?

Apps learn about roles in four ways, and most setups combine two or three of them:

| Way | How fresh | What it costs |
| --- | --- | --- |
| Claims in the token at sign-in | As of the sign-in | Nothing: the roles arrive with the person |
| The OpenID Connect userinfo endpoint | Current | A request that needs a valid access token |
| A management API, called by your server | Current | A server-to-server request, within rate limits |
| Pushed changes: webhooks or SCIM | Usually seconds | An endpoint you secure, retry and keep idempotent |

Token claims are the cheapest and go stale first, because a token shows the roles someone had when it was issued. The [JWT roles answer](https://loginos.io/answers/roles-permissions-in-jwt.md) covers lifetimes and verification. A pull from userinfo or a management API is current, but it costs a request, so cache the result for a few minutes.

Pushed changes are the fastest when your provider supports them. A webhook calls your endpoint when a role changes. SCIM (System for Cross-domain Identity Management, RFC 7644) is a protocol that creates and updates users and groups in other systems, mostly in enterprise setups.

A pattern that works for most products:

1. Read roles from the token at sign-in and keep them in the app’s own session.
2. Re-read them every few minutes, and before sensitive actions like refunds or role changes.
3. End the session when the re-read says the account is blocked.

## What role should a new user get?

New accounts should get one minimal default role, like `member` or `viewer`, automatically. Never make a privileged role the default: one wrong default hands admin rights to everyone who signs up.

Grant staff roles by hand, to a named person, ideally after a second person approves. Inside your product, the person who creates a workspace or team usually becomes its owner. That ownership is a per-tenant fact your app records, not a global role.

## How do you keep access small and auditable?

Least privilege means each person holds only the permissions their job needs. Four habits keep it that way:

- **Split read and write.** `shop:orders:read` and `shop:orders:refund` are separate permissions, so support can look at an order without refunding it.
- **Review regularly.** Every quarter, list who holds each powerful role, and remove what nobody can explain.
- **Log every change.** Record who changed whose roles, when, and from what to what. If your provider doesn’t keep that history, write it from the code that calls its API.
- **Offboard in one place.** Block the account at the identity provider, so sign-in stops in every app at once, then end the sessions your apps hold. Removing roles alone isn’t enough: the person can still sign in, and every app that grants something to any signed-in account still lets them in.

## How loginos does it

In loginos, your product is a **platform**, and one platform can run on any number of domains with one pool of members. Roles and permissions belong to the platform, so they apply on all of its domains. Put your main app, admin console and shop on one platform, even on `app.example.com`, `admin.example.net` and `shop.example.org`, and all three read the same roles. [Domains](https://loginos.io/docs/domains.md) explains how to add them.

- **Define** permissions and roles in the console’s **Roles** tab. Permission keys allow lowercase letters, digits and `_ . : -`, so `shop:orders:refund` works. Role keys like `support-agent` group them.
- **Default roles** go to everyone who joins the platform.
- **Delivery**: the ID token, the token endpoint’s `user` object and `/oauth/userinfo` always carry `roles` and `permissions`. For current values, call userinfo with the access token, or `GET /api/v1/users/{sub}` from your server.
- **Changes**: use the console’s **Users** tab or the [Management API](https://loginos.io/docs/api.md). `GET /api/v1/roles` lists your roles with their permissions.
- **Offboarding** is blocking: sign-in stops on all your domains, existing tokens stop working at once, and the roles are kept for when you unblock.

```bash title="curl"
# Make Jane a support agent on every domain of the platform. The list replaces her roles.
curl -X PUT https://loginos.io/api/v1/users/SUB/roles \
  -u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET" \
  -H "content-type: application/json" \
  -d '{"roles": ["member", "support-agent"]}'

# When she leaves: no more sign-ins on any of your domains.
curl -X POST https://loginos.io/api/v1/users/SUB/block -u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET"
```

Some parts stay with you. Roles are per platform, so two products on two platforms have separate roles, even though people use the same loginos account on both. A platform has no organizations or tenants, so roles per customer workspace live in your database. loginos doesn’t push changes either: there are no webhooks and no SCIM, so your apps re-read roles from the API. If you need roles per organization, or provisioning from your customers’ directories, compare [providers with built-in roles](https://loginos.io/answers/auth-providers-with-roles-permissions.md). The [roles guide](https://loginos.io/docs/roles.md) has the details.

## Frequently asked questions

### Should each app define its own roles?

Each app should define its own permissions, prefixed with its name, because the app knows which actions it has. Roles work better in one central place, because they describe people’s jobs, and jobs span apps. A support agent needs to read users in the admin console and refund orders in the shop, under one role.

### How do I give users different roles in different workspaces?

Store a membership per workspace in your app: the user, the workspace and the role. Global roles in a token can’t say “admin here, viewer there” without one role per workspace, which doesn’t scale. Some providers have organizations with their own roles. loginos doesn’t, so with loginos that table lives in your database, keyed by `sub`.

### What is the difference between RBAC, ABAC and ReBAC?

RBAC (role-based access control) decides by the roles a person holds. ABAC (attribute-based access control) decides by attributes of the person, the resource and the request, like a department or the time of day. ReBAC (relationship-based access control) decides by relationships, like “Jane owns this document”. Many products use RBAC for global rights and ReBAC for single objects.

### How fast do role changes reach my apps?

As fast as the slowest channel you rely on. Claims in tokens change at the next sign-in or token refresh. A userinfo or management API request shows the change at once. Webhooks, where a provider has them, usually arrive within seconds. Blocking should take effect everywhere at once, so check for it on every re-read.

### How do I remove someone’s access to all apps at once?

Block or disable the account at your identity provider, so no app can sign them in again. Then end the sessions your apps already hold, through the same re-read that refreshes roles. With loginos, `POST /api/v1/users/{sub}/block` stops sign-in on every domain of the platform.

---

Updated 2026-10-14. Web page: https://loginos.io/answers/manage-roles-across-apps
