Skip to main content

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.

Overview

The project user model replaces the legacy /register-tenant flow. It separates three concerns that /register-tenant collapsed into a single call:
  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.
  2. Project users — an individual user inside an organization. Created once per user with /v1/api/projects/register-user.
  3. JWT tokens — issued per session via /v1/api/projects/user-token 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.
Legacy /register-tenant still works and remains supported. New integrations should use this flow. Existing integrations should migrate when they need multiple users per organization, shared customizations, or external-id-based identification.

Data Model

  • 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

1. Register an Organization (once per customer entity)

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
{
  "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.
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.

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)

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
{
  "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. They’re surfaced in row-level security (RLS) rules as {{user.properties.X}} placeholders. See Tenant Properties.

Common errors

StatusCause
400 Bad RequestMissing required field (name, projectId, userRoleId, projectOrganizationId).
404 Not FoundprojectOrganizationId doesn’t match any row’s id or external_id in your scope.
404 Not FounduserRoleId doesn’t belong to the given project.
409 ConflictA user with the same userId or the same (project, name) already exists.

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

POST /v1/api/projects/user-token
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{
  "userId": "0c1c4a3f-b2d4-4f1e-9c54-9e9f9f9f9f9f",
  "expiration": "1h"
}
Response
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Pass the token to <UpsolveDashboard token={...} />. 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.
/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.

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

How this differs from /register-tenant

Concern/register-tenant (legacy)/register-user flow
Users per organization1 (each call creates a new admin user inside a new tenant)Many (one organization, many users)
Returns a JWT directlyYesNo — call /user-token separately
Customer-provided identifiersNo — Upsolve assignsYes — pass externalId (org) and userId (user) of your choosing
Shared spaces / customizationsNo — each tenant has its own spacesYes — all users in one organization share the same spaces
Role re-useNo — permissions live on the tenantYes — one role per (organization, permission set), assigned to N users