Sluby

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.

CapabilityAdminMember
Upload, list, edit, retry and delete videosYesYes
Create and revoke playback ids, mint signed URLs and share URLsYesYes
Create, list, rotate and revoke the workspace's API keysYesYes
Create and manage webhook endpoints, read their delivery logsYesYes
Read library stats and daily play countsYesYes
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 cacheHealth 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 deploymentMissing objects of the workspace's videos only
Start a reconciliation runYesNo
Read process metrics as JSON (GET /api/v1/metrics)YesNo
List members, change roles, remove and restore members, make reset linksYesNo
Create, list and withdraw invitesYesNo

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, read and manage), and acts as an admin when the member's role is admin.
  • 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 401 and This 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, from BOOTSTRAP_API_KEY or the seed script.
  • AUTH_DISABLED. With AUTH_DISABLED on, every request acts as one local development account with the admin role and all scopes. A server with NODE_ENV=production refuses 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, 401 with Wrong 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 403 with This 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 gets 409 with reason: 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 gets 409 with This 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:user scope 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 with 409 when 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_signin in 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, with expired_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_at at 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 401 with a reason: missing (no cookie, or a cookie this server did not sign), expired, revoked (signed out here or elsewhere), or not_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_SECRET with 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 as XXXXX-XXXXX.
  • When checking a typed code, the server ignores case, spaces and dashes, and reads I and L as 1 and O as 0.
  • POST /api/v1/auth/setup takes 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 answers 409 with This 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.

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 404 with This link does not exist. Ask an admin for a new one. A used-up, expired or withdrawn link answers 410 with This link was already used., This link has expired. or This link was withdrawn., each followed by Ask an admin for a new one.

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_uses sets how many people can join with the link: 1 (the default), a number up to 1000, or null for anyone with the link until it expires or is revoked. The row counts joins in use_count and sets used_at when 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_uses set to null or to a number from 2 to 1000 answers 400 with An admin invite lets one person join. Invite more people as members., and the database refuses such a row as well.
  • expires_in_days is 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.

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 403 and This 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 409 with You 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.

On this page