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
| Who | How | Gets |
|---|---|---|
| Account | Email + password, a magic link, or single sign-on. A second factor on top, if set | Whatever the organisation and team roles say: up to full control |
| Guest | Session number + join password at /join, or a signed presenter/share link | The lobby as a viewer; a role link or PIN promotes them without an account |
| Display | Its own screen token, baked into the link | Read-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.
| Role | Scope | view | present | share | manage |
|---|---|---|---|---|---|
| Owner | Organisation | ✓ | ✓ | ✓ | ✓ |
| Org admin | Organisation | ✓ | ✓ | ✓ | ✓ |
| Member | Organisation | ✓ | - | - | - |
| Team admin | Team | ✓ | ✓ | ✓ | ✓ |
| Presenter | Team | ✓ | ✓ | ✓ | - |
| Viewer | Team | ✓ | - | - | - |
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
| Actor | How | Holds |
|---|---|---|
| Superadmin | A fleet-wide flag, not a membership | Every capability in every organisation. Entering a customer's org is logged under their own name |
| Guest: viewer | Session number + join password | view |
| Guest: sharer | The share PIN or signed share link | view + share |
| Guest: presenter | The presenter PIN or signed presenter link | view + share + present |
| Display | Its own screen token | Reads only what it is told to show. Never a login |
| Legacy password / PIN | data/config.json, seed org only | Admin 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
- Sessions last 7 days and slide: every request pushes the expiry forward, so a panel left open all day never expires under you. Only a genuinely idle session ages out.
- A guest session lasts 12 hours, and dies the moment its meeting ends: immediately, not at the next request.
- Sessions are stored in the database, so a restart or crash no longer signs anyone out.
- Every device you are signed in on is listed in MyConsole with its address and last-seen time, and can be revoked one at a time.
- Eight bad attempts against one email locks that pair for five minutes; a much looser ceiling applies per IP, so one busy office cannot lock out a whole organisation.
- Screen links carry their own token; Rotate token on a screen invalidates the old link immediately.
- Presenter and share links are per-session bearer credentials; Rotate either role link to invalidate copies without changing the session password or PIN.
Where the secrets live
| Secret | Stored | Why |
|---|---|---|
| Account password | scrypt hash, unreadable | It is the person's, and it is reused elsewhere |
| Join password, presenter PIN, share PIN | Readable in the database | An organiser has to read it back to put it in an invitation. Per-session and rotated freely |
| Screen token | Readable, shown only to a caller who can manage | It is the link: it has to be copyable |
| Presenter/share access link | HMAC-signed URL; the raw URL is not stored | A 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-lived | A stolen database must not yield working links |
| Two-step secret | Readable in the database | The server has to generate the same codes the phone does. Recovery codes, which it does not, are hashed |
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
-
Register the application with the provider and give it this redirect URI,
exactly:
<name>is whatever you call the provider in the next step:
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 matchhttps://your-host/auth/oidc/<name>/callbackpublicUrlindata/config.json. -
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:
Issuers for the other two: Entra is{ "publicUrl": "https://your-host", "oidc": { "google": { "label": "Google", "issuer": "https://accounts.google.com", "clientId": "xxxx.apps.googleusercontent.com", "clientSecret": "xxxx" } } }https://login.microsoftonline.com/<tenant-id>/v2.0, Okta ishttps://<your-org>.okta.com. Add"scope"only if you need more thanopenid email profile. Several providers can sit side by side; each key becomes its own button. - Restart the service.
/loginnow shows the button. -
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.
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.