Web Apps
A web app is a browser experience. A visitor opens it. It does not install on a device. This page explains how a web app is served. It explains how a session starts and stays alive. It explains what the app can call over @phystack/hub-client. It explains the security and operating limits you plan around.
For a full walkthrough of building and publishing a web app, see the Web App tutorial.
What A Web App Is
Section titled “What A Web App Is”A web app is an ordinary web app. It only needs @phystack/hub-client to connect to the platform. Any web stack that can run it works. The official template uses React with Vite. The difference from a screen app is how people reach it. You install a screen app on a device. A visitor opens a web app through a session link or a QR code, straight in their own browser.
How A Web App Is Served
Section titled “How A Web App Is Served”One endpoint is one Web twin. When you publish a web app, the platform unpacks the build into public storage. It serves the build through a web endpoint at a public URL:
https://w.phystack.com/{tenant-slug}/{name}/The visitor’s browser loads a small boot.json pointer. This pointer tells hub-client it is running in web mode, and where to mint a session. Deploying a new build to an installation repoints every enabled web endpoint automatically. You do not need to recreate them.
The Session Lifecycle
Section titled “The Session Lifecycle”A web session goes through four stages. Someone mints a code. The visitor opens it. The app exchanges the code for a session. The session refreshes itself until it reaches its deadline.
1. Minting A Code
Section titled “1. Minting A Code”A claim code can come from three places. It can come from the Console (“Get session link”). It can come from the CLI (phy web-endpoint code <endpoint>). Or a screen app can subscribe to subscribeWebSessionCode and render the result as a QR code.
2. The Visitor Opens The Code
Section titled “2. The Visitor Opens The Code”The visitor opens the endpoint’s URL with a #code= fragment attached. They scan a QR code, or they open a link directly.
3. Token Exchange
Section titled “3. Token Exchange”The browser calls the mint route with the code. PhyHub responds with a socket token, a refresh token, and the session’s absolute deadline:
POST /api/v1/web-sessions{ "urlId": "<tenant-slug>/<name>", "code": "<code>" }The response is a flat object: token, expiresIn, expiresAt, refreshToken, deadlineInSeconds. The refresh token is one-time-use. It rotates on every refresh.
4. Refresh Persistence Across Reloads
Section titled “4. Refresh Persistence Across Reloads”hub-client stores the rotating refresh grant in the browser’s localStorage, keyed by the endpoint’s urlId. The session survives a page reload. It survives a tab discard. It survives the browser closing and reopening. It survives all of this up until the session deadline.
The browser follows a fixed order when it opens the app. A fresh #code= in the URL always wins over a stored session. If there is no fresh code, a stored session resumes silently. If a fresh code has expired, the app falls back to the stored session, if one exists. If neither exists, the app throws the exact error "No session code in the URL — open this web app via its QR code or session link (#code=...)."
What Ends A Session
Section titled “What Ends A Session”Two different clocks apply. They mean different things.
- The socket token is short-lived. The client reconnects invisibly with a refreshed token when the token is about to expire. This is a refresh cadence, not the session length. The visitor never notices it happening.
- The session deadline is the hard end. The platform fixes it the moment the code is redeemed. Refreshing the socket token never extends it. Once the deadline passes, the refresh grant stops working. Storage is cleared. The app should prompt the visitor to rescan.
Disabling an endpoint also ends every live session on it, within one token TTL.
Endpoint Settings
Section titled “Endpoint Settings”An endpoint has six settings that control how its sessions behave. Set them at creation, or change them later with phy web-endpoint update.
If you skip a setting, the CLI and the Console each apply a different default:
| Setting | CLI default | Console default | What it controls |
|---|---|---|---|
| Scopes | twin:read, twin:message | twin:read only | Which categories of calls the session can make. See Scopes below. |
Code reuse (codeMode) | Single-use | Multi-use | Single-use: the code is consumed the first time it’s redeemed. Only one visitor can use it. Multi-use: the code stays redeemable until it expires. It suits a shared kiosk QR that many visitors scan. |
Code TTL (codeTtlSeconds) | 300 seconds | 300 seconds | How long a minted code stays valid before it expires unused. |
Max sessions (maxSessions) | 100 | 100 | The cap on concurrent open sockets for this endpoint, not a cap on mints. |
Session TTL (sessionTtlSeconds) | 900 seconds | 900 seconds | How often the socket token refreshes in the background. |
Max session duration (maxSessionDurationSeconds) | 3600 seconds | 3600 seconds | The hard session deadline, fixed at code redemption. Refreshes never extend it. |
PhyHub itself sets no default. It requires every setting explicitly. Whichever tool creates the endpoint, CLI or Console, decides the value.
What Happens At The Session Cap
Section titled “What Happens At The Session Cap”maxSessions counts concurrent sockets connected to the endpoint, across every pod. It does not count how many codes have been minted. When a new session would push the endpoint over the cap, PhyHub sends a webAppAuthenticated ack with status: error and the message “Session limit reached for this endpoint”. It then disconnects the socket.
Every denial in this system looks the same to the caller, by design. Redemption failures at the mint route all return one opaque 401, an expired code, a consumed code, and an unknown code are indistinguishable. A denied read or message during a live session, one outside the allowlist or scope, returns the same generic denial.
What A Web Session Can Call
Section titled “What A Web Session Can Call”A web session is a consumer. It reads and messages twins. It does not own or provision them. The tables below list everything PhyHub exposes over the web socket, and the calls that throw because they are device-only.
What Works
Section titled “What Works”| Call | What it does |
|---|---|
getTwinById / subscribeTwin | Reads and subscribes to a twin’s data, if it’s on the session’s allowlist. |
Twin messaging (emit, to().emit(), on()) | Sends and receives events and actions with twins on the allowlist. |
getIceServers | Gets TURN credentials for WebRTC. Requires the media:view scope. |
| Event signals | Sends event signals from the session. |
getSettings() | Reads the session’s resolved settings, delivered on connect from the endpoint’s own Web twin. |
getWebInstance() | Reads the session’s own Web twin, and sends messages to other twins with to(twinId).emit(...). This also carries WebRTC signaling. |
getPeripheralInstance() | Reads a peripheral twin’s document, listens to the owner’s events, and sends actions or media dials to it, as a consumer. Owner-only methods still throw. |
isWebAppSession() | Lets shared app code check whether it is running as a web session. |
What Throws
Section titled “What Throws”| Call | Why |
|---|---|
getDeviceStatus(), getInstance(), getDeviceInstance(), getDeviceNetworks(), setScreenInstanceReportedProperties(), updateReportedProperties() | Device-session only. |
createPeripheralTwin (via getInstance()) | Device-session only, it needs getInstance(). |
createWebSessionCode(), subscribeWebSessionCode() | Device-session only. Screen and edge apps use these to mint codes for a web endpoint. |
initializeSignals() without explicit init params | Needs explicit init params in a web session, there is no device status to derive them from. |
Scopes And The Allowlist
Section titled “Scopes And The Allowlist”An endpoint has three scopes: twin:read, twin:message, and media:view. PhyHub applies no default. The caller must set every scope explicitly. The CLI and the Console fill in different defaults (see the table above).
twin:readlets the session read and subscribe to twins on its allowlist. A session can always read its own Web twin, with or without this scope.twin:messagelets the session send twin messages. This includes messages to its own Web twin.media:viewlets the session callgetIceServersfor WebRTC TURN credentials. Without this scope, camera feeds connect only when a direct or STUN-assisted connection works.
The allowlist has two parts: the twin picker references in the endpoint’s resolved settings, and the endpoint’s own Web twin. Nothing else is included. The schema defines the picker fields. The operator selects the twins. Together they set exactly what the session can reach.
The allowlist is fixed in the session’s token at mint time. A settings change does not apply to a live session. The session picks up the change at its next reconnect.
Overrides at the device or space level also change the allowlist. The allowlist uses the endpoint’s resolved settings, and an endpoint id is a valid override target.
Operating Limits
Section titled “Operating Limits”Code minting is rate limited, per client IP and per endpoint. A client that mints too fast receives HTTP 429 with the message “Too many requests”. The exact limits can change, so do not hardcode them in your app. Treat a 429 as a signal to back off and retry, not as a bug.
Next Steps
Section titled “Next Steps”- Build your first web app for a full walkthrough.
- Settings Schemas to define the twin pickers that set a web session’s allowlist.
- Twin Messaging and Real-Time Channels for what a web session can send and receive.