User and auth management
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.
- Sign in to the web portal as an Admin, open Users, and click Invite User.
- Enter the email, display name, and role, then click Send invite. You don’t set a password.
- 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.
- 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.
- 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
| Service | What it manages | Roles |
|---|---|---|
| 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) |
|---|---|---|
| Admin | Full access to every console page. | Yes. |
| Producer | Redirected 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 endpoint | Who can call it |
|---|---|
| POST /api/projects | Admin |
| PUT /api/projects/:id | Admin |
| PUT /api/projects/:id/conventions | Admin |
| PUT /api/projects/:id/settings | Admin |
| PUT /api/projects/:id/validators | Admin |
| POST /api/projects/:id/material-catalog | Admin (the matching GET isn’t admin-only) |
| POST /api/validator-catalog | Admin (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.
| Action | Where 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
- Local passwords: Invite User creates the account with no password and emails the invitee a link to set one. The link is valid for 7 days. The email goes out over the instance’s own SMTP server (see Self-hosted install).
- Directory sign-in (LDAP): Invite User creates the account with the role you chose and sends no email. The person signs in with their directory credentials, and that first sign-in attaches to the account, provided that Dousen has a user search base (see Self-hosted install). The banner reads "<email> can now sign in with their directory credentials".
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.
- The invitee becomes a
memberof the organization in DousenControl, whatever role you choose. Their first cloud sign-in attaches to the pending row, which keeps the role you chose on the studio instance. - The invitation expires after 7 days and holds a seat until then. The row then shows an Invite expired badge and stops counting against seats, but it still attaches with its chosen role if the person signs in later.
- Remove cancels the emailed invitation first, so the link stops working, and then deletes the row. If the person has already joined the organization, Dousen refuses and tells you to remove them from the team in DousenControl.
- Resend invite appears only on an expired invite. It asks DousenControl for a new invitation and counts the row against seats again.
Invite messages
After you click Send invite, the Users page shows one of these messages:
| Message | What 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.
- You can’t deactivate your own account, the last active admin, or a pending invite. Use Remove for an invite.
- On a self-hosted instance, deactivating a user frees their seat, and Reactivate takes one again. If no seat is free, it fails with the seat-limit message. On a cloud studio, deactivating doesn’t free a seat. To free it, remove the member from the Dousen organization in the cloud dashboard.
- Inviting the email address of a deactivated user fails and tells you to use Reactivate.
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.
| Route | Who | Purpose |
|---|---|---|
| GET/POST /portal/forgot-password | Anyone, not signed in | Request 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:
| Message | What 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
| Method | How it works | Role 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.
- LDAP: group membership decides the role only on the first sign-in, and not at all if an admin created the account first with Invite User. Removing someone from the admin group later doesn’t remove their Admin role. If Dousen has no user search base, it can’t read groups, so everyone starts as an Artist. See Directory sign-in.
- Cloud: the DousenControl role applies only when the exchange creates a new account. An account invited from the Users page keeps the role the admin chose. The organization’s Owner is always a protected Admin on the studio instance; a verified cloud sign-in records that ownership in Dousen.
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.
| Role | Can do |
|---|---|
owner |
The organization’s creator. Has the same invite, billing, and license permissions as admin. |
admin | Manage invites, billing and checkout, and the organization’s license. |
member | Belongs 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.
| Action | Endpoint |
|---|---|
| Send an invite | POST /api/orgs/:id/invites (owner or admin) |
| List pending invites | GET /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
| Plan | Default seats |
|---|---|
| trial | 5 |
| free | 5 |
| starter | 25 |
| pro | 100 |
| enterprise | 500 |
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)
| Endpoint | Purpose |
|---|---|
| POST /api/auth/login | Local-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/login | Browser login form. |
| POST /portal/logout | Clears the session cookies. |
| GET/POST /portal/forgot-password | Self-service reset request. |
| GET/POST /portal/reset-password | Self-service reset completion, and the page invite links open. |
| GET /portal/users, POST /portal/users | List users, and invite a user (Admin). |
| PUT /portal/users/:id/role | Change a user’s role (Admin). |
| POST /portal/users/:id/send-reset | Send a password-reset email to a user (Admin). |
| POST /portal/users/:id/deactivate | Deactivate a user (Admin). |
| POST /portal/users/:id/reactivate | Reactivate a deactivated user, if a seat is free (Admin). |
| POST /portal/users/:id/resend-invite | Send 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)
| Endpoint | Purpose |
|---|---|
| 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/authorize | Shows 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/invites | Send and list invites (owner or admin). |
| POST /api/invites/accept | Public. The invitee sets their own password. |