# Kiosk Build, sign and install Officer's iOS and Android apps on your own registered devices. What EAS does, self-hosted. - **A catalogue of builds**, newest per app per platform, with an Install button that hands `itms-services://` to iOS or a plain URL to Android. - **A per-user key** that authorises artefact downloads. Held in memory by the app and re-read on demand, so rotating one costs nothing. - **Device records** — who owns which iPhone or iPad, and whether Apple knows about it yet. ## Status — read before installing **Nothing here has run.** Ported on 2026-08-18 from the standalone service that was called OAS. The authenticated API and the two panels exist and typecheck; no endpoint has answered a request. ### What works, in principle `GET /apps`, `GET /apps/:appName`, `GET /key`, `POST /key/rotate`, `GET /install/:appName/:platform/:version`, `POST /installs`, and the device and build verbs. All under `/api/kiosk`, all requiring a platform session. ### What is missing, and why **Installing will not work yet.** Four requests in the flow arrive with no session — iOS `installd` fetching the manifest and the `.ipa`, the device posting its UDID, and Safari downloading a `.mobileconfig` — and a plugin cannot serve an unauthenticated route today. `UNPROTECTED_API_PREFIXES` is hardcoded in the platform and plugin routers mount only behind the account gate. So `GET /install/...` returns a correct-looking URL pointing at a route that does not exist. That is deliberate: the URL shape is what the mobile app builds against, and it stops being a dead end the day the platform gains `unauthenticatedPaths`. Those routes then land in `api/public.ts`, kept separate from `api/router.ts` so the two surfaces cannot be confused by anyone reading either. **Enrolment is not ported** for the same reason — it is entirely public routes. **Artefacts arrive by rsync**, into `$OFFICER_ROOT/data/kiosk/`. The plugin records metadata about files already on disk; it does not accept uploads, because the platform's shared upload route caps at 5MB and no `.ipa` will ever satisfy that. ## Notes - `PUBLIC_URL` must be **HTTPS with a certificate iOS already trusts**. It fetches the manifest and binary itself and declines silently otherwise, which looks exactly like a broken build. - Table names are prefixed `kiosk_`. There are no migrations — see the note at the top of `db/schema.ts`. - The plugin is currently `appName: kiosk`. If the plugin *storefront* takes that name, this one is renamed; see `docs/kiosk-ecosystem.md` in the platform.