User and auth management

Updated October 2026

Describes how to invite and manage users on a studio instance, how roles and sign-in methods work, and how cloud organization membership relates to them.

Invite an artist and verify access

These steps work on a self-hosted instance and on a cloud studio. For how each type of studio handles the invite, see Invites on a self-hosted instance and Invites on a cloud studio.

  1. Sign in to the web portal as an Admin, open Users, and click Invite User.
  2. Enter the email, display name, and role, then click Send invite. You don’t set a password.
  3. Read the message above the list. With local passwords or on a cloud studio, the new row shows a Pending invite badge until the person sets a password or accepts the cloud invitation.
  4. Ask the artist to accept the invite and sign in to DousenDesktop against the studio URL. On a self-hosted instance with local passwords, they set a password of at least 8 characters from the emailed link.
  5. Have them select the intended project. Check that their normal artist workflow works and that admin-only configuration isn’t available to them.

If the invite is refused, check the available seats and your own role. For a directory or cloud account, fix the directory account or the organization membership instead of setting a local password. To create the first admin on a new self-hosted instance, see Self-hosted install.

After you change a role, ask the person to sign in and check that they have the access you expect.

Cloud roles and studio roles

ServiceWhat it managesRoles
DousenControl Organizations, billing and subscriptions, and the DousenCore instance that serves each tenant. Studios don’t run this service themselves. owner, admin, member
DousenCore instance Projects, conventions, validators, changelists, source assets, and the studio’s own users. This is the "studio instance" in the rest of these docs. Admin, Producer, Artist

Cloud roles and studio roles are stored in separate databases and checked independently. A DousenControl role only sets a person’s starting role on an instance, the first time they sign in through the cloud. See Which role wins.

Roles on a studio instance

The organization creator remains an active Admin. Their row is marked Organization creator and Admin (protected), with no role-change or deactivation control. The first local account created on a fresh self-hosted instance is also a protected Admin. Other users can change roles, provided at least one active Admin remains; pending invites and deactivated users do not satisfy that requirement.

The whole web portal (/portal/*) requires the web_portal license feature, and every console page requires the Admin role. The console covers projects, conventions, validators, pipeline settings, users, changelists, assets, the health dashboard, the validation history page, and the material-catalog viewer.

Role Console (/portal/*) Self-service account (/portal/account)
AdminFull access to every console page.Yes.
ProducerRedirected to /portal/account on any console route.Yes.
Artist Redirected to /portal/account on any console route. Yes.

The JSON REST API doesn’t redirect. Each admin-only endpoint checks the role itself and returns 403 to anyone else.

REST endpointWho can call it
POST /api/projectsAdmin
PUT /api/projects/:idAdmin
PUT /api/projects/:id/conventionsAdmin
PUT /api/projects/:id/settingsAdmin
PUT /api/projects/:id/validatorsAdmin
POST /api/projects/:id/material-catalogAdmin (the matching GET isn’t admin-only)
POST /api/validator-catalogAdmin (the matching GET isn’t admin-only)
Changelist submit and unshelve (/api/vcs/submit, /api/vcs/unshelve) The user who opened the changelist, or an Admin

Invite and manage users

Admins manage a studio instance’s users on the portal’s Users page. Screenshots on this page use sample accounts.

Users page with the Invite User form open, showing Email, Display Name and Role fields and a Send invite button, above a table of three accounts and one pending invite.
The Invite User form on a studio instance. The invited account shows a Pending invite badge and a Resend invite button until the person sets a password.
ActionWhere to do it
List users and check seats Open Users in the portal sidebar. The header shows the seat count, for example 4 / 10 seats used.
Open the invite form Click Invite User. When every seat is in use, this button is replaced by Upgrade plan.
Invite a user Fill in Email, Display Name, and Role, then click Send invite.
Resend an invite Click Resend invite in the row of a pending invite, and confirm. On a self-hosted instance, Core sends a new 7-day link, and the earlier link stops working.
Cancel a pending invite Click Remove in the row of a pending invite, next to Resend invite. A pending local invite is deleted and frees its seat. For a cloud invite, see Invites on a cloud studio.
Change a user’s role Choose the new role in that user’s row, then check the saved selection.
Deactivate a user Click Deactivate in the user’s row, and confirm. See Deactivate and reactivate a user.
Reactivate a user Click Reactivate in the row of a user with a red Deactivated badge, and confirm. The user must sign in again.
Send a password-reset link Click Send reset link in that user’s row, then read the message that replaces the button.

Invites on a self-hosted instance

Invites on a cloud studio

On a cloud studio, Invite User creates the row with the role you chose and asks DousenControl to send a team invitation. DousenControl owns the invitation email, the seat check, and the acceptance step. No password is set in the portal, and the portal’s sign-in page redirects to cloud sign-in.

Invite messages

After you click Send invite, the Users page shows one of these messages:

MessageWhat it means
Invitation sent to <email> The invite email was handed to the mail server (self-hosted) or to DousenControl (cloud).
<email> was created, but SMTP isn't configured on this instance — no email was sent. The account exists, but nothing was emailed. The message adds that the link is logged only at debug level, with the hint RUST_LOG=info,dousen_api::email=debug. Configure SMTP and click Resend invite, or take the set-password link from the server log (see Self-hosted install).
<email> was created, but the invite email failed to send SMTP is configured but the send failed. Read the server log, fix the cause, then click Resend invite.
<email> was created, but invite emails for this address are rate-limited right now Each address can receive only 3 invite or reset emails per hour. Click Resend invite again in a few minutes.
<email> is already a member of the org — their role will apply when they first sign in to Dousen. No invite email was sent. Cloud only. The person already belongs to the organization in DousenControl, so no invitation was sent.

If a user with that email already exists, the invite is refused with "A user with <email> already exists". Change their role in the list instead, or use Resend invite if they haven’t set up their account yet. With directory sign-in, inviting an existing email updates that account’s role and display name instead.

Integration reference: invite request

The invite form posts email, display_name, and role (admin, producer, or artist), with no password field. Like every portal form, the request needs the session cookie and the session’s CSRF token, as a csrf_token form field or an X-CSRF-Token header:

HTTPPOST /portal/users HTTP/1.1
Content-Type: application/x-www-form-urlencoded

csrf_token=…&email=jane%40studio.com&display_name=Jane+Doe&role=artist

The response redirects to /portal/users?feedback=<code>&email=<email>. The feedback codes are invite_sent and cloud_invite_sent (invitation sent), invite_smtp_unconfigured (created, no email because SMTP isn’t configured), invited_directory (directory account created), cloud_already_member, invite_failed, and invite_rate_limited. The page shows nothing for any other value.

Deactivate and reactivate a user

When someone leaves the studio, click Deactivate in their row. The user can’t sign in by any method, and their sessions end on their next request. Their changelists and history stay. A deactivated row shows a red Deactivated badge and a Reactivate button.

Seats

Inviting a user checks the seat limit before it creates the row, and pending invites count. On a self-hosted instance, the limit is the seat count in your license, and an invite holds a seat from the moment you send it. If no seats are left, the invite fails with "Seat limit reached (n/n). Upgrade your plan to add more users." On a cloud studio, DousenControl checks the organization’s limit, and a refused invite shows "seat limit reached — upgrade your plan to invite more members". To free a seat on a self-hosted instance, Deactivate a user or Remove a pending invite. See Deactivate and reactivate a user.

An instance without a valid license is locked and refuses portal requests. Only DOUSEN_DEV_UNLICENSED=true, for local development, removes the seat limit.

Password reset and recovery

Users can request a reset link themselves, or an admin can send one from the Users page. Both use the same mechanism.

RouteWhoPurpose
GET/POST /portal/forgot-passwordAnyone, not signed inRequest a reset link by email.
GET/POST /portal/reset-password Anyone with a valid token Use the token to set a new password. Invite links open this page too.
POST /portal/users/:id/send-reset Admin Send a reset link to a user from the Users page.

A reset or invite token is a random value that DousenCore stores as a SHA-256 hash. A reset link is valid for 1 hour and an invite link for 7 days. A token allows 5 attempts, and each email address can request 3 links per hour, counting invites and resets together. A new password must be at least 8 characters. The self-service page shows the same response whether or not the account exists, so it doesn’t reveal which emails have accounts.

A message replaces the Send reset link or Resend invite button:

MessageWhat it means
Reset link sent SMTP is configured and nothing failed. To avoid revealing which accounts exist, this message also appears when no email was sent: for a directory or cloud account, or when the address reached its hourly limit.
SMTP not configured — no email sent. The link is logged only at debug level (RUST_LOG=info,dousen_api::email=debug) No email was sent. To get the link, see Self-hosted install.
Failed to send — check server logs SMTP is configured, but the send failed.
Rate-limited — try again in a few minutes Resend invite only. The address reached its hourly limit.
This user isn't waiting on an invite Resend invite only. The person already set a password or signed in, so there’s nothing to resend.

Directory and cloud accounts have no local password, so reset links don’t apply to them. Those users always sign in through their directory or the cloud. The Send reset link button still appears on their rows, but no email is sent.

Sign-in methods

MethodHow it worksRole on first sign-in
Local password DOUSEN_AUTH_MODE=local (the default). DousenCore checks the password against the instance’s own Argon2 hash. The role the admin chose in Invite User.
Directory (LDAP/AD) DOUSEN_AUTH_MODE=ldap. Dousen binds directly as the signing-in user, so you don’t need a service account, and empty passwords are always rejected. The account is created on the first successful bind. Members of DOUSEN_LDAP_ADMIN_GROUP become Admin and everyone else becomes Artist. This path never assigns Producer, and it needs a working user search base and filter to read group membership. An admin can create the account earlier with Invite User and choose its role.
Browser OAuth (cloud) DousenDesktop opens the browser to DousenControl’s /oauth/authorize, using PKCE (S256) and a local loopback callback server. Set by the cloud organization role, applied when the exchange runs (next row).
Cloud exchange After OAuth sign-in, DousenControl issues a short-lived, Ed25519-signed exchange token. DousenCore accepts it at POST or GET /portal/auth/exchange (the portal) and POST /api/auth/exchange (Desktop). The account is created on the first exchange. Cloud owner and admin become Admin and everyone else becomes Artist, unless an admin created the account first with Invite User.

A user who belongs to more than one cloud organization sees a studio picker before reaching a specific instance, both in the interactive login and in the OAuth flow.

Which role wins

For users other than the organization creator, the role you set in Dousen wins over a role from a directory or the cloud. A directory group or a DousenControl role only sets the starting role when Dousen first creates the account, and no later sign-in changes it.

To change another user’s role afterward, use the role selector on the Users page. The creator cannot be demoted, deactivated, or removed.

Disabling an account in your directory blocks new sign-ins, but DousenDesktop sessions that already exist keep refreshing until the refresh token expires. To end them, also Deactivate the user in Dousen.

Dousen reads a user’s role from the database on every request, so a demoted admin loses admin access immediately.

Sessions, tokens, and single-device enforcement

On a DousenCore instance, the access token is a JWT that lasts 15 minutes. The refresh token is an opaque value that lasts 30 days and rotates on every use: refreshing revokes the old refresh token and issues a new pair. DousenDesktop refreshes automatically when it gets a 401.

Single-device enforcement: a refresh token can carry a device_id. If a refresh or heartbeat arrives with a different device_id from the one stored for that session, the server answers 409 Conflict with "session active on another device", and the app shows that message. So signing in on another machine stops the first session from refreshing.

Cloud organizations and studios (DousenControl)

DousenControl manages organization membership, separately from any DousenCore instance.

RoleCan do
owner The organization’s creator. Has the same invite, billing, and license permissions as admin.
adminManage invites, billing and checkout, and the organization’s license.
memberBelongs to the organization, with no invite, billing, or license access.

Owners and admins invite people from the organization dashboard, or from a studio’s Invite User button (see Invites on a cloud studio). An admin never sets the invitee’s password.

ActionEndpoint
Send an invitePOST /api/orgs/:id/invites (owner or admin)
List pending invitesGET /api/orgs/:id/invites (owner or admin)
Accept an invite POST /api/invites/accept (public, because the invitee isn’t a user yet)

DousenControl checks the seat limit before it creates an invite. An invite expires after 7 days. Its role can only be admin or member, because nobody becomes an owner by invite. The invitee sets their own password when accepting, at least 8 characters long. The seat check and the insert happen together, so two simultaneous accepts can’t oversubscribe a seat-limited organization.

Cloud plans and seat limits

PlanDefault seats
trial5
free5
starter25
pro100
enterprise500

A new subscription starts with these seat counts. The quantity Stripe reports for a purchase later replaces the number, and an admin can set an override. A trial lasts 14 days, then the organization moves to the permanent free plan.

Each DousenCore instance reports its current user count to DousenControl in a daily heartbeat. If the count is over the seat limit for three consecutive heartbeats, DousenControl suspends the organization (further heartbeats get a 403) and emails the owner. A later heartbeat that’s back under the limit reactivates it. Only a seat overage triggers this suspension. A subscription that’s only past_due on payment isn’t suspended for that reason.

Auth endpoint reference

DousenCore (per studio instance)

EndpointPurpose
POST /api/auth/loginLocal-account login. Returns a JWT access and refresh pair.
POST /api/auth/refresh Exchanges a refresh token for a new access and refresh pair (rotates on use).
POST /api/auth/heartbeat Device-session heartbeat. Returns 409 if another device now owns the session.
POST /api/auth/exchange Accepts a DousenControl exchange token and returns a session (used by Desktop).
GET/POST /portal/auth/exchange Accepts a DousenControl exchange token and starts a portal session.
GET/POST /portal/loginBrowser login form.
POST /portal/logoutClears the session cookies.
GET/POST /portal/forgot-passwordSelf-service reset request.
GET/POST /portal/reset-passwordSelf-service reset completion, and the page invite links open.
GET /portal/users, POST /portal/usersList users, and invite a user (Admin).
PUT /portal/users/:id/roleChange a user’s role (Admin).
POST /portal/users/:id/send-resetSend a password-reset email to a user (Admin).
POST /portal/users/:id/deactivateDeactivate a user (Admin).
POST /portal/users/:id/reactivateReactivate a deactivated user, if a seat is free (Admin).
POST /portal/users/:id/resend-inviteSend a pending invite again with a new link (Admin).
POST /portal/users/:id/remove-pending Cancel and delete a pending invite (Admin). Returns 409 for any other user.
GET /portal/account, POST /portal/account/* Self-service profile, password change, and session revocation, for any signed-in role.

DousenControl (cloud)

EndpointPurpose
POST /api/auth/login Organization login. Returns a choose_org list instead of a token if the user belongs to two or more organizations.
GET /oauth/authorizeShows the browser login form for the Desktop PKCE flow.
POST /oauth/authorize/submit Checks the password, shows the studio picker if needed, and issues a short-lived auth code.
POST /oauth/token PKCE exchange. Checks the code verifier and issues the DousenCore-bound exchange token.
POST /api/orgs/:id/invites, GET /api/orgs/:id/invitesSend and list invites (owner or admin).
POST /api/invites/acceptPublic. The invitee sets their own password.