Build A Web App
What Are Web Apps?
Section titled “What Are Web Apps?”Web apps are browser experiences that run on a visitor’s own phone: companion remotes, sign-up forms, product handoffs. They’re not installed on a device, they’re opened.
A web app is a plain static web build. It uses React and @phystack/hub-client. Its app code looks much like a screen app’s. The difference 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 Web Apps Work
Section titled “How Web Apps Work”When you publish a web app, the build is unpacked into public storage and served through a web endpoint. One endpoint is one Web twin. The endpoint’s public URL looks like this:
https://w.phystack.com/{tenant-slug}/{name}/Visitors open the URL with a #code= fragment. hub-client exchanges the code for a session and receives the twin and its settings automatically. See Web Apps for how the session lifecycle works under the hood.
Web sessions are consumers. A session can only read and message the twins that the endpoint’s settings reference. The settings schema sets this security boundary. Codes expire in 5 minutes by default. Whether a code is single-use or multi-use depends on how you created the endpoint, see the defaults table below. Sessions persist across page reloads.
Prerequisites
Section titled “Prerequisites”Official PhyStack templates and examples use React with TypeScript. This is the stack we’ll be working with throughout this tutorial.
Before you begin, make sure the following are set up and ready to go:
A PhyStack Tenant
Section titled “A PhyStack Tenant”You’ll need access to a PhyStack tenant. If you don’t yet have one, please refer to the Tenant Setup guide.
Local Development Environment with the PhyStack CLI Installed and Configured
Section titled “Local Development Environment with the PhyStack CLI Installed and Configured”If you haven’t set up the CLI on your machine yet, refer to the Dev Environment Setup guide.
PhyStack Simulator
Section titled “PhyStack Simulator”The simulator is required to run your app locally during development. See the Simulator reference for setup and usage.
Step-by-Step: Creating and Publishing a Web App
Section titled “Step-by-Step: Creating and Publishing a Web App”Once your environment is ready, you can create and publish your first web app. The flow is: scaffold a project, run it locally in the simulator, register the app in your tenant, then build and publish.
1. Scaffold the Web App Project
Section titled “1. Scaffold the Web App Project”phy app init my-web-app --type webThis clones the official web template (Vite + React + TypeScript) and patches it with your app’s name. Web apps don’t take a --lang flag, there’s only one official template.
Install the dependencies:
cd my-web-appbun installThe project layout matches a screen app: src/settings/index.json for local settings, schema.ts for the settings schema, App.tsx for the main component built on @phystack/hub-client.
2. Run App Locally with the Simulator
Section titled “2. Run App Locally with the Simulator”Start the simulator server in a separate terminal:
phy-simulator startThen launch the app against it:
bun run devThis runs a predev step that generates your local settings. It then hands off to phy-simulator run .. You don’t need a --type flag, the simulator auto-detects web from application-type in package.json. The simulator creates a local Web twin. It writes a simulator-owned public/boot.json pointing at itself. It prints “Web app dev loop ready” before launching the Vite dev server.
Open your browser at:
http://localhost:3000/#code=devAny code value works against the simulator, #code=dev is as good as any other. Reloading the page resumes the session automatically.
For full details on simulator commands and options, see the Simulator reference.
3. Register the App in Your Tenant
Section titled “3. Register the App in Your Tenant”phy app create my-web-app --type webThis registers the app definition in your tenant, a one-time step. The command is non-interactive and prints the app’s identifiers when it completes.
4. Build & Publish
Section titled “4. Build & Publish”bun run pubThe pub script runs your local build (typecheck, Vite production bundle, generated schemas) and then submits and publishes it. Web apps ship no container image, so there is no --push and no registry login involved.
Checkpoint
Section titled “Checkpoint”After completing this process, you’ll have:
- A working web app project on your local machine built with React
- A new app available in your tenant for installation
Making Your Web App Reachable
Section titled “Making Your Web App Reachable”A published app isn’t reachable by visitors on its own, you need an installation and a web endpoint. An endpoint can only be created after the installation has a deployed build, so the order below matters.
1. Create an Installation
Section titled “1. Create an Installation”- Log in to console.phystack.com
- Navigate to Apps > Developer and locate your app in the list
- Press the Install button and provide a name for your new installation
- After confirmation, you will be redirected to the Installation page
2. Deploy the First Build
Section titled “2. Deploy the First Build”- Navigate to the Builds tab on your Installation page
- Deploy the build you published in the previous section
The installation now has a deployed build, so you’re ready to create a web endpoint.
3. Create a Web Endpoint
Section titled “3. Create a Web Endpoint”You can create a web endpoint from the Console or the CLI.
From the Console: open your installation’s Web endpoints tab and use the create drawer.
From the CLI:
phy web-endpoint create --set displayName="My Web App" \ --set installationId=<installation-id> --set spaceId=<space-id> \ --set urlId=my-web-appcreate takes the same generic write flags as other phy create commands. Use repeatable --set key=value flags (JSON-typed), or pass --from-file with a JSON or YAML body. urlId is just the name segment. The platform prefixes it with your tenant slug to build the full public path {tenant-slug}/{name}. Leave urlId out and the platform derives it from displayName instead. The resulting URL id is permanent either way. A deleted endpoint’s URL id cannot be reused. Pick it carefully.
PhyHub applies no defaults. The API requires every setting explicitly. Whichever caller creates the endpoint decides the policy. The CLI and the Console fill in different defaults for settings you don’t pass:
| Setting | CLI default | Console default |
|---|---|---|
| Scopes | twin:read, twin:message | twin:read only |
| Code reuse | Single-use | Multi-use |
| Code TTL | 300 seconds | 300 seconds |
| Max sessions | 100 | 100 |
| Session TTL | 900 seconds | 900 seconds |
| Max session duration | 3600 seconds | 3600 seconds |
4. Open the Web App
Section titled “4. Open the Web App”Press Get session link on the endpoint in the Console for a copyable link with a countdown, or generate one from the CLI:
phy web-endpoint code <endpoint>Opening the link on any phone or browser is equivalent to scanning a QR code. No device needed.
Checkpoint
Section titled “Checkpoint”After completing this part of the tutorial, you should have:
- An installation with a deployed build
- A web endpoint with a working session link you can open from any browser
Connecting to a Screen App
Section titled “Connecting to a Screen App”Most web apps aren’t opened directly, they’re handed off from a screen app running on a device, usually through a QR code. This section connects a screen app to the web app you just built.
A web session’s twin allowlist comes only from the web endpoint’s own settings, plus its own Web twin. A screen app’s twin picker pointing at the Web twin does not grant the web session anything. It only gives the screen app a handle to mint codes and message the web app. This handoff needs a twin picker on both sides. Add one to the screen app, so it can find and message the web endpoint. Add one to the web app, so the web session can reach the screen app’s twins.
1. Add a Twin Picker to the Screen App’s Settings
Section titled “1. Add a Twin Picker to the Screen App’s Settings”In the screen app’s settings schema, add a twin picker field pointing at Web twins:
/** * @title Web Remote * @ui twinPicker * @uiOptions { "twinTypes": ["Web"] } */webRemote?: TwinPicker;The operator picks the web endpoint’s twin in the installation’s Settings tab. The screen app uses this reference to find the web twin, mint codes for it, and send it messages.
2. Add a Twin Picker to the Web App’s Settings
Section titled “2. Add a Twin Picker to the Web App’s Settings”The web session needs its own twin picker to reach the screen app’s twins. Add this picker to the web app’s own settings schema, not the screen app’s. Add the same kind of field to the web app’s schema.ts. Point it at whichever twins the phone needs to talk to:
/** * @title Screen Twin * @ui twinPicker * @uiOptions { "twinTypes": ["Screen"] } */screenTwin?: TwinPicker;The operator sets this reference on the web installation’s Settings tab. This reference, not the screen app’s, ends up in the web session’s allowlist.
3. Render a QR Code for the Web Session
Section titled “3. Render a QR Code for the Web Session”In the screen app’s code, subscribe for a live session code and render the URL as a QR:
const subscription = client.subscribeWebSessionCode( { twinId: settings.webRemote.id }, (state) => { if (state.status === 'active') { setQrUrl(state.url); // full public URL including #code= } else { setQrUrl(null); // never render a dead code } },);// subscription.stop() on unmountThe subscription handles rotation before expiry and re-minting after reconnects, so you never need to poll for a fresh code yourself. state.url is the complete endpoint URL with the code already attached, ready to encode as a QR (the office demos use the qrcode-generator package).
subscribeWebSessionCode is available to device sessions, screen and edge apps, it’s not available to web or cloud sessions.
4. Test the Handoff Locally with the Simulator
Section titled “4. Test the Handoff Locally with the Simulator”Start the web app first, it takes port 3000:
bun run devThe simulator prints the web app’s Twin ID on startup, copy it. Then start the screen app in a separate terminal, Vite will pick another port since 3000 is taken:
bun run devPut the web app’s Twin ID into the screen app’s local settings for the twin picker field, in src/settings/index.json:
{ "webRemote": { "id": "<twin-id>", "ref": "twin" }}The simulator answers the code subscription with a localhost URL, so test the handoff in a browser on the same machine. Scanning from a phone requires the deployed endpoint from the previous section.
Iterate
Section titled “Iterate”To ship a code change: publish a new build, then deploy it from the Console.
bun run pubDeploying a new build in the Console repoints every enabled web endpoint on that installation automatically. You don’t need to recreate them.
Success!
Section titled “Success!”Your web app is published, reachable through a web endpoint, and wired up to a screen app for a QR code handoff.
You’ve successfully gone from creation to deployment, iteration, and production publishing, all with full integration into the PhyStack platform.
Checkpoint
Section titled “Checkpoint”After completing this part of the tutorial, you should have:
- A published web app reachable through a session link
- A screen app that renders a live QR code linking to a web session
- An understanding of how codes, sessions, and endpoint scopes fit together
Next Steps
Section titled “Next Steps”Now that your web app is set up, you can: