User Portal
The User Portal is what a user sees when a workstation is idle: a grid of application cards curated by their operator, served from their organization's own subdomain.
It is rendered by src/ui_portal.ts at /home on a tenant host. The root / serves the organization homepage (src/ui_org_home.ts) instead, and /portal was removed.
What a user sees
+---------------------------------------------------------------+
| Digital Learning Lab [portal_title] |
| Select an approved page to begin [portal_subtitle] |
| |
| Math & Science [category] |
| +-------------+ +-------------+ +-------------+ |
| | ๐ | | ๐ฌ | | ๐ | |
| | Khan Academy| | PhET Sims | | GeoGebra | |
| +-------------+ +-------------+ +-------------+ |
| |
| Computer Science |
| +-------------+ +-------------+ |
| | ๐ฑ | | ๐ป | |
| | Scratch | | Code.org | |
| +-------------+ +-------------+ |
| |
| For technical assistance, raise your hand. [portal_footer]|
+---------------------------------------------------------------+
Cards are grouped by category and ordered by order_index. Everything on the page โ title, subtitle, description, footer โ is per-organization copy the operator controls.
There is no search box, no address bar, and no way to reach anything that is not on this page or on the allowlist.
Where the cards come from
GET /api/portal-sites is public, scoped to the host tenant. A card is:
| Field | Purpose |
|---|---|
title | The card's label |
url | Where it goes |
domain | Derived from the URL; feeds the effective allowlist |
category | Groups the cards |
icon | An emoji |
thumbnail_url | Optional card image |
order_index | Display order |
is_active | Whether it renders |
Operators manage them under Portal Apps in the dashboard. โ Admin Console Guide
Adding a card authorises its domain
This is the part worth understanding. The allowlist a workstation enforces is not just the permanent list an operator typed in โ it is that list unioned with every portal app's host, computed per heartbeat by buildEffectiveWhitelist().
tenant_whitelist rows khanacademy.org, example.com
+
portal_sites.domain scratch.mit.edu, phet.colorado.edu
+
active broadcast host (when one is active)
=
effective allowlist delivered in every telemetry response
So an operator who adds a Scratch card does not also have to add scratch.mit.edu to the allowlist. A card that appeared but did not work would be an obvious and constant support burden, and this removes it.
Navigation from the portal
Clicking a card navigates top-level, natively. It is not an iframe โ Khan Academy, YouTube, and Scratch all enforce X-Frame-Options: SAMEORIGIN and would fail with ERR_BLOCKED_BY_RESPONSE.
Once on the site, the kiosk's own chrome comes from the browser extension: an auto-hiding 44 px bar with Home, Back, Forward, Reload, and a status shield, injected into a closed Shadow DOM.
Home returns to the portal. That is the user's only reliable route back, which is why the bar exists at all.
When a broadcast is active
A broadcast overrides the portal. Workstations navigate to the operator's URL and stay there: the URL is stored on the tenant row, so a machine that reboots mid-page rejoins the same page.
At the broadcast root the extension suppresses Alt+Left and Backspace, so a user cannot reverse out of it. The operator's Reset to portal clears the broadcast and returns everyone to the launcher.
single_url mode
An organization can skip the portal entirely. In single_url mode workstations go to one destination โ an LMS, an assessment platform, a library catalogue โ and there is no launcher at all.
Use portal mode when users choose among approved resources; single_url when they should be in exactly one place.
Customisation
| Setting | Where it appears |
|---|---|
portal_title | The main heading |
portal_subtitle | Under the heading |
portal_description | Room or context label, e.g. "Computer Science Lab 304" |
portal_footer | Bottom of the page, e.g. "For technical assistance, raise your hand." |
| Organization name | Throughout |
Set under Settings โ Customization.
Escaping
Every one of those strings, and every card title and URL, is attacker-controlled from the platform's perspective: it arrives through registration or the admin console.
- Server-side, each interpolation goes through
escapeHtml()orescapeJson(). - Client-side, DOM nodes are built and
textContentassigned โ never concatenation intoinnerHTML. - URLs pass
safeHttpUrl(); a card whose URL is nothttp(s)is rejected on creation.
The test suite renders the portal with a hostile app title and asserts it comes back inert.
Privacy
A user's session leaves nothing behind. overlayroot="tmpfs" sends every write to RAM: browser profile, cache, history, downloads, and any file they saved. At power-off it is all gone, and the next user gets a genuinely fresh machine.
What the organization does see is a screen thumbnail every three seconds while a workstation is powered on, visible to any organization admin of that organization, plus the currently active URL. That is the supervision the product exists to provide โ worth being explicit about in a lab's acceptable-use policy.
โ Admin Console Guide ยท Kiosk Hardening ยท Control Plane Internals