Contents

Access & sessions

Organisations, teams and sessions

One deployment serves many organisations. An organisation owns its content library and its people. Inside it, a team owns the physical screens: a display bolted to a wall belongs to a room, not to a meeting. A session is one meeting: it has a nine-digit number, a join password, a presenter PIN and a share PIN, and while it is live it claims the team's screens. Nothing crosses an organisation boundary.

Manage all of it at /console: people and their roles, teams, sessions and their credentials, and the devices you are signed in on.

Three ways in

WhoHowGets
AccountEmail + password, a magic link, or single sign-on. A second factor on top, if setWhatever the organisation and team roles say: up to full control
GuestSession number + join password at /join, or a signed presenter/share linkThe lobby as a viewer; a role link or PIN promotes them without an account
DisplayIts own screen token, baked into the linkRead-only access to what it is told to show. Never a login

Capabilities, not job titles, decide what a request may do: view, present, share, manage. A guest holding the presenter PIN and a signed-in presenter both hold present, so they take exactly the same path through the code.

Roles and what they may do

Four capabilities decide everything: view (read the session and its content), present (advance slides, set screen state, drive the agenda, screen-share), share (put a screen-share on a display), and manage (upload, delete, rename, settings, screens, PINs). A role is just a bundle of these, and the highest role a person holds wins: extra rows never subtract.

Roles come on two axes. An organisation role applies across the whole org; a team role applies to one team's sessions and screens. An org owner or org admin is automatically a team admin of every team inside the org, so they never lock themselves out.

RoleScopeviewpresentsharemanage
OwnerOrganisation✓✓✓✓
Org adminOrganisation✓✓✓✓
MemberOrganisation✓---
Team adminTeam✓✓✓✓
PresenterTeam✓✓✓-
ViewerTeam✓---

A plain org member with no team role can read but not drive; a role on a team is what grants present and manage. Manage is per team: a team admin of Team A holds no rights over Team B unless they are also an org admin, or Team A is attached to Team B's session.

Not accounts

ActorHowHolds
SuperadminA fleet-wide flag, not a membershipEvery capability in every organisation. Entering a customer's org is logged under their own name
Guest: viewerSession number + join passwordview
Guest: sharerThe share PIN or signed share linkview + share
Guest: presenterThe presenter PIN or signed presenter linkview + share + present
DisplayIts own screen tokenReads only what it is told to show. Never a login
Legacy password / PINdata/config.json, seed org onlyAdmin password = full manage; presenter PIN = view + present + share. Retire once a real account exists

One override sits ahead of all of it: if the organisation requires two-step verification and a person has not enrolled, they hold no capability at all (superadmin included) until they set it up.

PINs cascade

Set a presenter PIN or a share PIN on the organisation and every team and session inherits it. Set one on a team and it wins for that team's sessions. Set one on the session and it wins there. The console shows which level each value came from, so it is never a guess.

The same credential panel can create a separate signed Presenter access link and Share access link for each session, with a QR code for each. They skip the join password and PIN, grant only their named role in that session, and can be rotated separately. They are bearer credentials: send each one only to the person who needs it. A session marked members-only still requires that member to sign in.

Login sessions

Where the secrets live

SecretStoredWhy
Account passwordscrypt hash, unreadableIt is the person's, and it is reused elsewhere
Join password, presenter PIN, share PINReadable in the databaseAn organiser has to read it back to put it in an invitation. Per-session and rotated freely
Screen tokenReadable, shown only to a caller who can manageIt is the link: it has to be copyable
Presenter/share access linkHMAC-signed URL; the raw URL is not storedA per-install signer and a per-session nonce recreate it; it is a bearer credential with one role and one session, so rotate it when shared accidentally
Emailed links (magic, invite, reset, verify)SHA-256 hash, single use, short-livedA stolen database must not yield working links
Two-step secretReadable in the databaseThe server has to generate the same codes the phone does. Recovery codes, which it does not, are hashed

Email

Magic links, invitations, verification and password resets go out over SMTP, configured under an smtp key in data/config.json. With no SMTP configured every flow still works: the link is written to the log and shown to an owner in the console, which is what makes this testable on a box that cannot send mail.

Two-step verification

Optional per person, under Account in the console: scan the QR code with any authenticator app, type one code back to prove it works, and keep the ten recovery codes. They are shown once and never again.

Every route into the account then asks for it: a magic link and a password reset included. Otherwise switching it on would make an account weaker for anyone whose mailbox is the thing that got breached. An owner can require it of a whole organisation under People → Security; that takes effect at once, and anyone without a second factor keeps their session but holds no rights until they enrol.

Single sign-on

Any OpenID Connect provider (Google, Microsoft Entra, Okta) configured under an oidc key in data/config.json. The sign-in page grows a Continue with… button on its own once one is set up.

An organisation can claim an email domain, and anyone arriving from that domain joins it as a member automatically. Only ever as a member: an email domain is evidence of employment, not of authority. An address the provider has not verified is refused outright, and an account with a second factor still has to give a code afterwards.

Setting one up

  1. Register the application with the provider and give it this redirect URI, exactly: <name> is whatever you call the provider in the next step:
    https://your-host/auth/oidc/<name>/callback
    Google calls it an Authorized redirect URI on a Web application OAuth client; Entra calls it a Web redirect URI; Okta calls it a Sign-in redirect URI. The host must match publicUrl in data/config.json.
  2. Add the provider to data/config.json. Nothing is provider-specific beyond these four values: everything else is read from the issuer's own discovery document:
    {
      "publicUrl": "https://your-host",
      "oidc": {
        "google": {
          "label": "Google",
          "issuer": "https://accounts.google.com",
          "clientId": "xxxx.apps.googleusercontent.com",
          "clientSecret": "xxxx"
        }
      }
    }
    Issuers for the other two: Entra is https://login.microsoftonline.com/<tenant-id>/v2.0, Okta is https://<your-org>.okta.com. Add "scope" only if you need more than openid email profile. Several providers can sit side by side; each key becomes its own button.
  3. Restart the service. /login now shows the button.
  4. Claim your email domain so first-time arrivals land somewhere real: MyConsole → People → Security → Single sign-on. That block only appears once a provider is configured. Enter acme.com, not @acme.com, and pick the role such arrivals get. One domain belongs to one organisation; a superadmin can also set it per org in the panel.
Without a claimed domain, SSO still works for people who already have an account here: the first sign-in links the two by verified email address. Everyone else is refused with sso-no-org and needs an invitation. That is deliberate: a provider button is not an open door.

People link and unlink their own provider accounts under MyConsole → Sign-in. When a sign-in bounces back, the reason is in the URL (sso-unverified-email, sso-no-org, sso-expired) and the detail is in Logs under sso.*.

The old shared password

The single admin password and presenter PIN in data/config.json still work, scoped to the original organisation. The PIN gets view, present and share; the password adds manage. Retire them once you have a real account: run node tools/create-owner.js <email> on the server, follow the one-time link it prints, then delete adminHash from config.json.

Sharing from outside the building

A presenter on the room's own network connects straight to the screens. A presenter anywhere else goes through a relay (TURN) running on this server, because two home or mobile networks cannot normally reach each other. If a share shows a black screen and then Connection failed, that is the relay path failing: check Logs for rtc.failed, which lists what each side managed to offer.

Locked out completely? Delete data/config.json and restart: a fresh password is printed once to the log. Nothing else is lost.

Last updated