Accounts, roles and sessions
How Sluby models a team, what admins and members can do, and how sign-in, sessions, invites and reset links work.
This page explains who can do what on a Sluby server: how one install maps to one team, what the admin and member roles allow, how the backend decides who made a request, and how sign-in, Studio sessions, the setup code, invite links and reset links work. For the Studio screens that manage members, see Invite and manage members. For how an operator sets up sign-in, see Set up sign-in.
One install is one team
A Sluby install serves one team, called the workspace. Every video, API key, webhook endpoint and upload belongs to the workspace, and every caller (a member's Studio session, an API key, or the local development account) acts as the workspace. Each person who can sign in is a row in the users table with a role. The role decides what a person can do, not which videos they see: every member sees the whole library.
Roles and what each can do
There are two roles, admin and member.
| Capability | Admin | Member |
|---|---|---|
| Upload, list, edit, retry and delete videos | Yes | Yes |
| Create and revoke playback ids, mint signed URLs and share URLs | Yes | Yes |
| Create, list, rotate and revoke the workspace's API keys | Yes | Yes |
| Create and manage webhook endpoints, read their delivery logs | Yes | Yes |
| Read library stats and daily play counts | Yes | Yes |
Read system status (GET /api/v1/system/status) | Full status: database, Redis, the Sia indexer's account usage and hosts, the last reconciliation run, the delivery cache | Health summary only: overall status, version, database, Redis and indexer health, the indexer's network and its host pool summary |
Read the reconciliation report (GET /api/v1/reconciliation) | The whole deployment | Missing objects of the workspace's videos only |
| Start a reconciliation run | Yes | No |
Read process metrics as JSON (GET /api/v1/metrics) | Yes | No |
| List members, change roles, remove and restore members, make reset links | Yes | No |
| Create, list and withdraw invites | Yes | No |
An endpoint reserved for admins answers a member with 403 and Only an admin may use this endpoint. API conventions lists the scopes an API key needs for each endpoint. Deletion and reconciliation describes the reconciliation report.
The backend reads the role on every request. A role change applies to the member's sessions and API keys on their next request.
Who a request acts as
Every request under /api/v1 (except the /api/v1/auth routes, the OpenAPI document at /api/v1/openapi.json and the reference page at /api/v1/docs) and every tus upload request goes through the same check, in this order:
- Studio session. A valid session cookie wins over an API key. A session carries all three scopes (
upload,readandmanage), and acts as an admin when the member's role isadmin. - API key. A key carries the scopes it was created with. It acts with its creator's role as it stands at that request. Removing the creator stops the key with
401andThis API key's creator was removed from this workspace; restoring the creator makes the key work again. A key with no creator acts as an admin: these are keys made on the server itself, fromBOOTSTRAP_API_KEYor the seed script. - AUTH_DISABLED. With
AUTH_DISABLEDon, every request acts as one local development account with the admin role and all scopes. A server withNODE_ENV=productionrefuses to start with it (see Configuration).
Sign-in methods
A member can have more than one way to sign in. Each way is a row in user_identities, linked to the member's users row.
Email and password
- The server trims and lowercases the email, and each email belongs to a single account.
- A new password must have 10 to 256 characters.
- The server stores passwords as scrypt hashes (N=32768, r=8, p=1, 16-byte random salt).
- A wrong email and a wrong password get the same answer,
401withWrong email or password. An unknown email costs the same hashing time as a known one, so response timing does not reveal which emails exist. - A removed member's correct password gets
403withThis account was removed from this workspace. - Changing the sign-in email (
POST /api/v1/auth/email/change) also needs the current password, counts against the same attempt limits, and keeps the password. Another account's email gets409withreason: email_taken. - A member changes their own name with
PATCH /api/v1/auth/me. - Changing the password (
POST /api/v1/auth/password/change) needs the current password, and ends every other session of the account while keeping the current one. A member who signs in with GitHub only has no password yet and gets409withThis account has no password yet. Ask an admin for a reset link to set one.
GitHub
The server offers GitHub sign-in only when it has both GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET set. GET /api/v1/auth/config reports it in methods.github.
- Sluby asks GitHub for the
read:userscope only. - Sluby identifies a GitHub account by its numeric account id, not its login, so renaming a GitHub login does not change who it signs in as. Sluby stores the login for display and refreshes it at each sign-in. The member's name stays the one they chose.
- A member removes GitHub from their account with
DELETE /api/v1/auth/identities/github. Sessions that signed in with GitHub end. The server refuses with409when this browser signed in with GitHub (current_sign_in) or when GitHub is the account's only way to sign in (last_sign_in). - Signing in with GitHub never creates an account. Sign-in refuses a GitHub account that belongs to no member (
not_a_member). A person joins with GitHub through an invite link, and an existing member adds GitHub from their account settings. - The sign-in keeps its state in a cookie named
sluby_signinin the browser that started it (path/api/v1/auth, HttpOnly, valid for 10 minutes). The callback accepts only the state that cookie holds, so it refuses a callback opened in another browser, or after 10 minutes, withexpired_handshake.
Sessions and cookies
Each Studio sign-in creates a row in studio_sessions with the member, the sign-in method used, the browser's user agent (first 500 characters) and its expiry.
The browser gets the sluby_session cookie (its attributes). The cookie value holds the session row id, the account id and the expiry, signed with HMAC-SHA256 using SESSION_SECRET.
On every request the backend checks the signature and the expiry, then that the session row is neither ended nor expired, then that the member is not removed. Because the backend checks the row each time, the server can end a session at any moment.
- Lifetime. A session lasts 7 days from sign-in. Using it does not extend it.
- Last seen. The backend updates the session's
last_seen_atat most once every 5 minutes. The Settings page shows it in the list of signed-in browsers. - Ending sessions. A session ends when the member signs out (
POST /api/v1/auth/logout), ends one session (DELETE /api/v1/auth/sessions/{id}), signs out everywhere (DELETE /api/v1/auth/sessions, which leaves API keys untouched), changes their password (every other session ends), uses a reset link (every session ends), or is removed by an admin (every session ends). - Why a session stopped working. A request with a cookie that no longer works gets
401with areason:missing(no cookie, or a cookie this server did not sign),expired,revoked(signed out here or elsewhere), ornot_allowed(the member was removed). - Cleanup. Ended sessions keep their rows for 30 days, then the hourly sweep deletes them.
- Changing SESSION_SECRET. The backend signs every cookie with
SESSION_SECRET, so changing it signs every member out. Security model lists what else depends on it.
First admin and the setup code
A fresh install has no admin. While sign-in is on and no admin who can sign in exists, the backend writes a setup code to its log at every start, and GET /api/v1/auth/config reports setup_required: true. An admin counts as able to sign in when the account is not removed and has at least one sign-in method; the local development account of an AUTH_DISABLED run does not count.
- The backend derives the code from
SESSION_SECRETwith HMAC-SHA256, so only someone who can read the server's logs or environment has it. It is 10 characters from the Crockford base32 alphabet, shown asXXXXX-XXXXX. - When checking a typed code, the server ignores case, spaces and dashes, and reads
IandLas1andOas0. POST /api/v1/auth/setuptakes the code, a name, an email and a password, creates the first admin and signs them in. Once an admin who can sign in exists, setup answers409withThis server already has an admin. Sign in instead., so the code stops working the moment it is used.
Set up sign-in describes where the operator finds the code.
Invites and reset links
Admins hand out two kinds of links: invite links, which let new people join, and reset links, which let one member set a password. Both are rows in the invites table and share these rules:
- The token is 32 random bytes in base64url (43 characters). The server stores only its SHA-256 hash, so the table alone is not enough to join or to take over an account.
- The full URL appears once, in the response that creates it. No later response shows it again.
- A reset link works once, for 7 days. An invite link works for 1, 7 or 30 days (7 by default) and for as many people as the admin chose (see Invite links).
- Using a link locks its row for the transaction, so two browsers using its last place at the same moment cannot both succeed.
- A link that does not exist answers
404withThis link does not exist. Ask an admin for a new one.A used-up, expired or withdrawn link answers410withThis link was already used.,This link has expired.orThis link was withdrawn., each followed byAsk an admin for a new one.
Invite links
An admin creates an invite with a role (POST /api/v1/invites, endpoint reference). The link has the form https://sluby.example.com/studio/join/<token>. Each new member joins with a name, email and password, or with GitHub.
max_usessets how many people can join with the link: 1 (the default), a number up to 1000, ornullfor anyone with the link until it expires or is revoked. The row counts joins inuse_countand setsused_atwhen the last allowed person joins.- Only a member invite can let more than one person join, because anyone can pass a link on. An admin invite with
max_usesset tonullor to a number from 2 to 1000 answers400withAn admin invite lets one person join. Invite more people as members., and the database refuses such a row as well. expires_in_daysis 1, 7 or 30, and 7 when left out.- A GitHub account that already belongs to a member cannot join a second time, and that attempt does not count as a join.
Admins list the invites that still let someone join, with max_uses and uses for each, and can withdraw any of them. Withdrawing stops new joins. People who already joined stay members.
Reset links
A reset link lets one member set a new password. An admin creates it for a member (POST /api/v1/members/{id}/reset-link, endpoint reference), and an operator who is locked out can print one on the server for an account that signs in with an email (Make a reset link from the command line). The link has the form https://sluby.example.com/studio/reset/<token>.
- A member who already has a password sets a new one.
- A member who signs in with GitHub only sets their first password and chooses the email they will sign in with.
- Using the link ends every session the member had, then signs the current browser in.
- The API refuses a reset link for a removed member with
403andThis account was removed from this workspace.
Removing members and the last-admin protection
Admins remove members rather than delete them:
- Remove. The member's row stays, so their API keys and history keep a name. They can no longer sign in, every session they have ends at once, and their API keys stop working. An admin cannot remove themselves; the API answers
409withYou cannot remove yourself. Ask another admin. - Restore. The member comes back with the role they had, and their API keys work again.
The API refuses a role change or a removal that would leave the workspace with no admin who can sign in, with 409 and This is the last admin who can sign in. Make someone else an admin first. These changes run one at a time, so two admins demoting each other at the same moment cannot both succeed.