Kiosk Mode
Kiosk mode is a simplified backoffice at /kiosk, for people who need to keep a
website and their own account running without learning the full administration
interface.
It is an alternative view of k0smos, not a second application: every action it
offers is performed by the same code the full backoffice uses, and every page
requires the same permission the equivalent /admin page requires. Kiosk mode
can only ever show less than the backoffice — never more.
The surface is part of core, so it exists on every tenant. What decides whether anyone can see it is the ACL, not a module switch: granting the gate permission always produces a working page, and revoking it always closes the surface.
What an operator sees
| Page | What it does | Requires |
|---|---|---|
/kiosk |
The list of features this operator may use | kiosk.use |
/kiosk/account |
Display name, language, light/dark and colours, password confirmation and two-factor authentication | kiosk.use |
/kiosk/site |
Public company details and the width of the public pages | kiosk.use + system.read, settings.company or settings.front_content |
The home page also shows cards for Pages, Blog, Information pages and Media when the tenant has those modules active and the operator holds their permission. Those cards are marked as opening the full backoffice, because that is where the editors live.
The kiosk surface has no entries for: users and roles, module activation, system settings, connectors and imports, queues, storage administration, MCP and agent configuration.
Setting up an operator
- Open
/admin/rolesand create a role from the Kiosk operator preset. It grantskiosk.useplus the minimum website permissions. - Assign that role to the person in
/admin/users/{id}. - The Kiosk mode entry appears in their sidebar under Tools, and
/kioskopens for them.
The preset intentionally leaves out system.write, because that single
permission unlocks every system settings page in k0smos. It grants two narrow
alternatives instead:
settings.company— read and save the public company details.settings.front_content— read and save the width of the public pages.
Each /kiosk/site panel is writable only for the operator who holds its code,
so someone with just settings.company saves the company panel and reads the
width panel. Both codes also work on the underlying endpoints, so nothing fails
on save. system.write still opens everything and remains the administrator's
tool; you should not need to hand it to an operator any more.
The preset also grants blog.delete and media.delete — the content an
operator routinely publishes and withdraws — but withholds page.delete and
info.delete: pages are structural and information pages carry the legal copy,
so removing one breaks navigation or compliance rather than tidying a mistake.
Password changes may require a code from email or an authenticator. The account page asks for that code before confirming the change and also offers the existing two-factor enrollment/reset controls. Company saves preserve settings not shown in the kiosk panel. Company and page-width saves record the operator and changed field names in the audit log without recording their values.
What the gate does and does not do
kiosk.use opens the shell. It is never a substitute for the permission behind
a page: /kiosk/site still demands one of the settings codes, and a card still
demands the permission of the page it links to. An operator holding only
kiosk.use sees the shell and their own account, and nothing else.
Conversely, holding a website permission without kiosk.use does not open
/kiosk at all. The two are required together.
By default kiosk mode confines nobody: an operator who also holds
administration permissions still reaches /admin normally, and the kiosk shell
links there. Kiosk is a simpler way in, not a lock — unless you ask for one.
Editing content and saving settings
Page, information-page and blog editors show the actions allowed for the current
operator. Creating or duplicating pages and deleting information pages remain
outside the kiosk preset. Blog deletion has its own blog.delete permission;
editing an article alone does not allow deletion. When updating an existing
installation, apply the Blog migrations and explicitly grant this permission
to existing custom roles that should delete articles. The migration preserves
all existing custom grants and labels.
Website saves confirm success only after a valid response. Switching to fluid layout preserves the last saved fixed width. Password confirmation returns keyboard focus to the password action after completion. If JavaScript is disabled or cannot load, the page explains that saving is unavailable and keeps the save buttons disabled; navigation and saved values remain available.
Locking an operator inside kiosk mode
Edit the role and tick Keep inside kiosk mode. Someone whose roles are all
confined then stays inside the surface: any other backoffice address sends them
back to /kiosk, and the shell stops offering the link to the full backoffice.
Use it for workstations that get handed between people. The role must have
kiosk.use before confinement can be enabled; disable confinement before removing
that permission. Confined logins open /kiosk directly.
Four things are worth knowing before you tick it:
- It removes no permission. The role keeps everything it holds; only where its holders may navigate changes. Untick it and they are back to normal immediately.
- It is unanimous. Someone who also holds one ordinary role keeps full access, so adding a role can never lock a person out by surprise.
- It never strands anyone. The pages their own kiosk cards link to stay open, along with their declared supporting editor operations. Public pages and media remain available for preview; unrelated account APIs stay closed.
- Confined logins get no "remember me". On a shared machine a long-lived session would hand the account to whoever sits down next, so the box is ignored for them.
System roles cannot be confined — confining the administrator role would lock everyone out of the backoffice with no way back in — so the option does not appear on them.
For developers: extending the surface
A module adds an entry by implementing
KioskCapabilityProviderModuleInterface and declaring a route name, never a
URL. The declaration is accepted only when all four of these hold:
- the route is registered — so a card whose module is inactive disappears;
- the route is a static GET page below
/adminor/kiosk, without URL/host parameters or a link to the whole backoffice root; - the route really enforces every permission the declaration names — so a card cannot be advertised as cheaper than the page behind it, cannot widen its audience past the route's own condition, and a route that enforces nothing can never appear;
- the module has not already contributed five entries.
Card visibility also checks every required route permission, its alternatives
and policy vetoes. Supporting editor APIs are listed explicitly in
operationRouteNames and must be registered and permission-guarded. Scoped
API tokens retain the user's confinement independently of their permission scope.
A contribution is therefore a link to a page the operator could already open
from /admin. It cannot escalate privileges, cannot render inside the kiosk
shell, and cannot displace a core page by reusing its id; a rejected declaration
is dropped rather than repaired, and logged at warning level so a missing card
is never silent.
Verification and deployment
The development review is complete. The retained checks passed 93 PHP tests (706 assertions), 19 JavaScript tests and 36 Chromium scenarios across Default and Sober. The browser suite includes 320/1440 px rendering, real company/width saves, email/TOTP confirmation and retry, synchronous code-input focus, dark focus, long identities, tooltips and disabled/missing JavaScript. Syntax, focused static analysis and documentation validation also pass.
To repeat the browser checks from the repository root:
node tools/benchmarks/kiosk-smoke.mjs reports/kiosk-smoke
Use Node 22 or newer, PHP and installed Chromium. Set K0SMOS_PHP_BINARY or
K0SMOS_CHROME_BINARY when their executable paths differ. Existing Default and
Sober build assets must be present; the runner never builds themes. Its loopback
servers, random tenant descriptors and temporary SQLite database are created
and removed automatically, including on failure. It accepts no runtime tenant
or target URL. Assertions, screenshots and server logs remain in the output
directory. An unsuccessful check exits nonzero.
Browser accessibility-tree checks verify labels, descriptions, live-region semantics and focus; they do not establish what a real screen reader speaks. No real screen reader was available for this review. During deployment, check the selected browser, screen reader, palette and actual workstation: navigate with Tab/Shift+Tab, complete password verification, hear the error/success announcements and confirm that focus returns to the password action. Treat any failure there as a concrete compatibility issue.
Apply the Blog permission migration as part of normal deployment and review explicit deletion grants on existing custom roles. Runtime tenant migrations, asset builds and activation were not performed by the development checks.
What kiosk mode is not
Kiosk mode is not digital signage. There is no unattended display, no paired device, no scheduled playlist and nothing public. That is a separate product question, still unanswered, recorded in ADR 0009.
The architecture, invariants and test coverage are in the complete reference § 30.