> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsolve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Roles and permissions

> What an embedded user may do on a dashboard, and how to set it.

Every embedded user is a **project user**, and what they may do on a dashboard comes from one place: their **role**. A role is a named set of permission flags in a project. Each project user has at most one.

## View-only unless a role grants editing

A role grants editing only through a flag that says so: `addChart`, `addFilter`, `createChart`, `editCharts`, `removeChart`, `addDashboard` or `canDuplicateDashboard` set to `true`. `readOnly` overrides all of them.

Every other user is **view-only**:

* a role stored with no permissions (`{}`), or with none of those flags;
* a role with `readOnly: true`;
* a project user with **no role at all**.

A view-only user sees the dashboard and uses its filters. They get no Add, Actions or Save buttons, cannot create or duplicate dashboards, and the API refuses their dashboard writes.

An edit never changes the shared dashboard. A user who can edit saves their changes to **their own copy**; the shared dashboard is edited by your builders in the Upsolve Hub.

<Note>
  This matches v1: a tenant registered without `tenantEditingPermissions` was view-only.
</Note>

## The permissions

"Unset" is what a flag means when the role does not mention it. A flag marked *on if the role edits* is on only when the role grants editing through another flag, and off on a view-only role.

| Flag | In the Hub | What it does | Unset |
| - | - | - | - |
| `readOnly` | Read Only | View only. Overrides every flag that grants editing, and turns off drill-down. | off |
| `addChart` | Add Chart | Add existing charts to a dashboard. Grants editing. | on if the role edits |
| `createChart` | Create Chart | Create new charts in the chart builder. Grants editing. | on if the role edits |
| `createChartSQL` | Create Chart with SQL | Write a chart's SQL by hand. No effect without `createChart`. | off |
| `editCharts` | Edit Charts | Edit the charts on a dashboard. Grants editing. | off |
| `removeChart` | Remove Chart | Remove charts from a dashboard. Grants editing. | on if the role edits |
| `addFilter` | Add Filter | Add new filters to a dashboard. Grants editing. Using the existing filters needs no permission. | on if the role edits |
| `addDashboard` | Add Dashboard | Create new dashboards. Grants editing. | on if the role edits |
| `canDuplicateDashboard` | Can Duplicate Dashboard | Duplicate a dashboard into a copy of the user's own. Grants editing. | on if the role edits |
| `canSetDefaultDashboard` | Can Set Default Dashboard | Set a dashboard as the user's default. | on |
| `drillDown` | Drill Down | Drill into a chart's data. Off whenever `readOnly` is on. | off |
| `aiCharts` | AI Charts | Create charts by asking AI. | off |
| `aiInsight` | AI Insight | Open the AI insights chat sidebar. | off |
| `canEditChartFromSession` | Can Edit Chart from AI Session | Edit an AI-generated chart from the agent chat. | on |
| `enableScheduleReport` | Schedule Report | Show the Schedule Report button, where scheduled reports are enabled for your organization. | off |
| `hideFilters` | Hide Filters | Hide the filter bar. | off |
| `hideHeader` | Hide Header | Hide the dashboard header. | off |
| `hideDownloadButton` | Hide Download Button | Hide the PDF download button. | off |
| `disableTabs` | Disable Tabs | Turn off the tabbed dashboard display. | off |
| `editMarketplace` | Edit Marketplace | Legacy. Has no effect. | off |

Spell out every flag you care about rather than relying on "Unset". A role that says what it grants reads the same everywhere and survives a change of defaults.

### Two common roles

A **reader** — sees and filters, changes nothing:

```json theme={null}
{ "readOnly": true }
```

An **editor of their own copies** — builds on top of a shared dashboard without touching it:

```json theme={null}
{
  "readOnly": false,
  "addChart": true,
  "createChart": true,
  "editCharts": true,
  "removeChart": true,
  "addFilter": true,
  "addDashboard": true,
  "canDuplicateDashboard": true
}
```

## Managing roles

In the Hub: **Project settings → Roles**.

With the API, using the same API key as [`embed-token`](/api-reference/endpoint/create-embed-token):

| | |
| - | - |
| List a project's roles | [`GET /v1/api/projects/{projectId}/user-roles`](/api-reference/endpoint/list-project-user-roles) |
| Create a role | [`POST /v1/api/projects/{projectId}/user-roles`](/api-reference/endpoint/create-project-user-role) |
| Read one | [`GET /v1/api/projects/{projectId}/user-roles/{projectUserRoleId}`](/api-reference/endpoint/get-project-user-role) |
| Change its name or permissions | [`PUT /v1/api/projects/{projectId}/user-roles/{projectUserRoleId}`](/api-reference/endpoint/update-project-user-role) |
| Delete it — its users are left with no role, so view-only | [`DELETE /v1/api/projects/{projectId}/user-roles/{projectUserRoleId}`](/api-reference/endpoint/delete-project-user-role) |

Role names are unique within a project; creating a second role with a taken name answers `409`.

## Giving a user a role

* **At sign-in.** [`embed-token`](/api-reference/endpoint/create-embed-token) takes `user.userRoleId`, applied on every call that sends it, and `user.defaultUserRoleId`, applied only to a user it creates. Send `userRoleId` when your product decides the role; send `defaultUserRoleId` when the role is managed in Upsolve after the first sign-in.
* **Later.** [`PUT /v1/api/projects/{projectId}/users/{projectUserId}`](/api-reference/endpoint/update-project-user) with `userRoleId`. `null` removes the role.

## When a change takes effect

A user's token carries their role's permissions as they were when it was issued. After you change a role, or move a user to another one, the user's **next token** has the new permissions; a token already issued keeps the old ones until it expires (`1h` by default). The API's checks on dashboard writes read the role when the write arrives, so they apply at once.

## Coming from the v1 SDK's `tenantEditingPermissions` prop

In v1 an integration could give its users permissions in two places: in the `register-tenant` call, and in the `tenantEditingPermissions` prop of the SDK component. The SDK laid the prop over the token's permissions in the browser. **The prop was never stored**, so a migration to v2 cannot see it: the roles it proposes come from what `register-tenant` was sent alone. In v2 an embed takes its permissions from the user's role and nothing else, so the prop's permissions have to go on the role.

v1 and v2 read a flag nobody set differently. v1 turned it off. A v2 role that grants editing takes the "Unset" defaults above for the editing flags it leaves out. So a v1 prop of `{ "addFilter": true }` copied onto a role as it is would also grant Add chart, Create chart, Remove chart and new dashboards. Write every editing flag out:

```json theme={null}
{
  "readOnly": false,
  "addFilter": true,
  "addChart": false,
  "createChart": false,
  "editCharts": false,
  "removeChart": false,
  "addDashboard": false,
  "canDuplicateDashboard": false
}
```

Add any other flag the prop set (`aiInsight`, `drillDown`, `hideHeader`, …) as the prop has it.

### Already migrated

[Update the role](/api-reference/endpoint/update-project-user-role) the migration created with the prop's permissions, written out as above. Tenants registered without permissions all share one migrated role, so this is usually one call. Every user on the role gets the permissions on their next token.

### Migrating now

Run the migration's two role steps through the API with the prop, then continue in the Hub. The Hub's migration page proposes roles without the prop.

1. **Propose the roles.** `POST /v1/api/migration/analyze-roles` with your prop as `sdkTenantEditingPermissions`. Each proposed role is what v1 showed its users: their v1 token with the prop laid over it, every editing flag written out.

   ```bash theme={null}
   curl -X POST "$UPSOLVE_API_URL/v1/api/migration/analyze-roles" \
     -H "Authorization: Bearer $UPSOLVE_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "organizationId": "<ORGANIZATION_ID>",
       "sdkTenantEditingPermissions": { "addFilter": true }
     }'
   ```

2. **Create them.** `POST /v1/api/migration/create-roles` with each proposed role's `permissions` and `tenantIds`, and a `name` of your choice.

   ```bash theme={null}
   curl -X POST "$UPSOLVE_API_URL/v1/api/migration/create-roles" \
     -H "Authorization: Bearer $UPSOLVE_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "organizationId": "<ORGANIZATION_ID>",
       "roles": [
         { "name": "Viewer with filters", "permissions": { … }, "tenantIds": ["…"] }
       ]
     }'
   ```

3. **Continue on the Hub's migration page** from *Create Project Users*. The role steps are marked done, so the page does not run them again.

A tenant registered after *Create Project Users* that already owns dashboards is given the role of a tenant registered with the same permissions, so it picks up the prop's permissions too. A tenant that first signs in after the migration gets its role from `register-tenant`, which never sees the prop: send `register-tenant` the prop's permissions from then on, and it finds the same role.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.