> ## 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.

# Project User Registration Flow

> Register users and organizations into a project so each user gets their own JWT, role, and customizable space.

## Overview

The project user model is the standard way to register users and organizations into a project. It separates three concerns into distinct calls:

1. **Project organizations** — the customer entity that a group of users belongs to (a company, a team, a workspace in your product). Created once per organization with [`/v1/api/projects/register-organization`](#1-register-an-organization-once-per-customer-entity).
2. **Project users** — an individual user inside an organization. Created once per user with [`/v1/api/projects/register-user`](#2-register-a-user-once-per-user-account).
3. **JWT tokens** — issued per session via [`/v1/api/projects/user-token`](#3-fetch-a-user-token-on-every-login--dashboard-load) using a user's ID.

Use this flow when you want many users to share one organization's spaces, roles, and dashboards — and when you want your existing user / organization identifiers to be the same identifiers Upsolve sees.

## Data Model

```mermaid theme={null}
flowchart LR
  ProjectOrg[("project_organization
  • id (UUID)
  • external_id (your ID)
  • name
  • properties")]
  User1[("project_user A")]
  User2[("project_user B")]
  User3[("project_user C")]
  Role[("project_user_role
  • permissions")]
  Space1[("space (app X)")]
  Space2[("space (app Y)")]

  ProjectOrg -- "1..N" --> User1
  ProjectOrg -- "1..N" --> User2
  ProjectOrg -- "1..N" --> User3
  ProjectOrg -- "1..N" --> Space1
  ProjectOrg -- "1..N" --> Space2
  Role -. "assigned" .- User1
  Role -. "assigned" .- User2
  Role -. "assigned" .- User3
```

* One **project organization** can contain many users.
* One **project user** belongs to exactly one project organization.
* One **role** is assigned to each user and defines their permissions.
* One **space** is automatically created per (application, project organization) — that's where users see and customize dashboards.

## End-to-End Flow

```mermaid theme={null}
sequenceDiagram
  participant App as Your App
  participant API as Upsolve API
  Note over App: User signs up
  App->>API: POST /v1/api/projects/register-organization (if new org)
  API-->>App: { projectOrganizationId, externalId }
  App->>API: POST /v1/api/projects/register-user
  API-->>App: { userId }
  Note over App: User logs in (every session)
  App->>API: POST /v1/api/projects/user-token { userId }
  API-->>App: { token }
  App->>App: render dashboard iFrame with token
```

## 1. Register an Organization (once per customer entity)

```http theme={null}
POST /v1/api/projects/register-organization
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{
  "projectId": "418c807a-83e7-4d95-8c8a-c7919dab180f",
  "name": "Acme Inc",
  "externalId": "acme-internal-uuid-1234",
  "properties": {
    "tier": "enterprise"
  }
}
```

**Response**

```json theme={null}
{
  "projectOrganizationId": "7c2b9d52-1f8a-4d31-bcaa-a91d4e5e9c10",
  "externalId": "acme-internal-uuid-1234"
}
```

### `externalId` — what it is and why you want it

`externalId` is any opaque string you control — typically the identifier your application already uses to reference the organization (e.g. a company UUID). Upsolve stores it alongside the Upsolve-issued `projectOrganizationId` and uses it to resolve subsequent calls.

**Why this matters:** wherever an Upsolve endpoint accepts `projectOrganizationId`, it now accepts either the Upsolve UUID **or** the `externalId`. The resolver tries the UUID lookup first and falls back to `externalId` within the same organization + project scope. So once an `externalId` is set, your application never needs to persist Upsolve's UUID — it can keep using the identifier it already has.

Use it when:

* You already have a stable identifier for your organizations and don't want to store a second one.
* You want `/register-user` calls to read naturally: `projectOrganizationId: "acme-internal-uuid-1234"`.

Leave it out (`externalId` is optional) if you're happy to persist Upsolve's UUID.

<Warning>
  `externalId` is unique within a project + organization. Two project
  organizations in the same project cannot share the same `externalId`. If you
  attempt to create one with a duplicate, you'll get a `409 Conflict`.
</Warning>

### Side effects of `/register-organization`

* A row is inserted into `project_organizations`.
* For every existing **application** in the project, a **space** is created. Each space is automatically populated with the application's published dashboard templates (lazy fork — templates render directly and only persist once a user customizes them).
* A `space_initialization_jobs` row is created per space for traceability.

If you add a new application later, spaces for existing project organizations are not retroactively created in this call — they're created the next time the app's first dashboard is opened.

## 2. Register a User (once per user account)

```http theme={null}
POST /v1/api/projects/register-user
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{
  "projectId": "418c807a-83e7-4d95-8c8a-c7919dab180f",
  "projectOrganizationId": "acme-internal-uuid-1234",
  "userRoleId": "62b91a8d-3e45-4a91-9f80-ab1234567890",
  "name": "Jane Doe",
  "userId": "0c1c4a3f-b2d4-4f1e-9c54-9e9f9f9f9f9f",
  "properties": {
    "department": "Finance",
    "region": "EU"
  }
}
```

**Response**

```json theme={null}
{
  "message": "User Jane Doe registered.",
  "userId": "0c1c4a3f-b2d4-4f1e-9c54-9e9f9f9f9f9f"
}
```

### Notes

* `projectOrganizationId` resolves polymorphically — pass either the Upsolve UUID or the `externalId` you set in step 1.
* `userId` is optional. If you supply your own UUID, that becomes the user's permanent identifier in Upsolve — you can re-use it on every future `/user-token` call without persisting Upsolve's user UUID. If you omit it, Upsolve generates one and returns it.
* `userRoleId` must refer to a project user role that exists in the same project. Roles define permissions (`addChart`, `editCharts`, etc.) and are reusable across users.
* `properties` are arbitrary JSON. Each key is surfaced in row-level security (RLS) rules as a `{{user.propertyName}}` placeholder (e.g. a `region` key becomes `{{user.region}}`). See [RLS & Schema Filtering](./rls-management).

### Common errors

| Status            | Cause                                                                                |
| ----------------- | ------------------------------------------------------------------------------------ |
| `400 Bad Request` | Missing required field (`name`, `projectId`, `userRoleId`, `projectOrganizationId`). |
| `404 Not Found`   | `projectOrganizationId` doesn't match any row's `id` or `external_id` in your scope. |
| `404 Not Found`   | `userRoleId` doesn't belong to the given project.                                    |
| `409 Conflict`    | A user with the same `userId` or the same `(project, name)` already exists.          |

## 3. Fetch a User Token (on every login / dashboard load)

```http theme={null}
POST /v1/api/projects/user-token
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{
  "userId": "0c1c4a3f-b2d4-4f1e-9c54-9e9f9f9f9f9f",
  "expiration": "1h"
}
```

**Response**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

Pass the token as the `jwt` query parameter on your dashboard or application iFrame `src` (see [Frontend Setup](/embedded-bi/deploy-dashboards/frontend-setup)). Tokens expire after the requested duration (default `1h`). Fetch a new token before expiry — changing the token causes the dashboard to refresh with the new user's permissions.

<Info>
  `/user-token` does not require the project ID or organization ID. The JWT
  payload is derived from the `project_users` row identified by `userId` — so
  the token automatically carries the user's project, organization,
  permissions, and properties.
</Info>

## Standard Login Loop

The simplest stable backend integration is:

1. **On user sign-up:** call `/register-organization` (if needed) and `/register-user`. Store nothing about Upsolve — just remember that the user is registered.
2. **On every page load that mounts an Upsolve dashboard:** call `/user-token` with the user's ID. Pass the returned token to the frontend.
3. **If `/user-token` returns `404 User not found`:** the user isn't registered yet — call `/register-user` then retry.

```ts theme={null}
async function getUpsolveToken(user: User): Promise<string> {
  try {
    const { token } = await upsolve.post("/projects/user-token", {
      userId: user.id,
    });
    return token;
  } catch (err) {
    if (err.status !== 404) throw err;

    await upsolve.post("/projects/register-user", {
      projectId: UPSOLVE_PROJECT_ID,
      projectOrganizationId: user.organization.id, // your internal ID — works as externalId
      userRoleId: roleForUser(user),
      name: user.name,
      userId: user.id,
      properties: { ... },
    });
    const { token } = await upsolve.post("/projects/user-token", {
      userId: user.id,
    });
    return token;
  }
}
```

## Security

* API key calls must go through your backend — never expose API keys to the browser.
* Tokens are scoped to a single project user. They cannot read other users' data, even within the same project organization, unless your role grants it.
* Always use HTTPS.
