Web Mount Point
Build browser-accessible interfaces for staff dashboards, operator desks, and public forms using the TelemetryOS web mount point.
Web Mount Point
The web mount point is a browser-accessible interface that runs outside the TelemetryOS device player and Studio admin portal. Unlike the render mount point (which displays on physical devices) and the settings mount point (which runs inside the Studio admin sidebar), the web mount point is accessed directly via URL in any standard browser — phones, tablets, and desktops.
Overview
- Accessible from any browser via a direct URL
- Used for operator desks, staff dashboards, and public-facing forms
- Requires
useSpaRouting: trueintelemetry.config.json - Uses the application name as the URL base path
- Store access: use
applicationscope by default;shared(namespace)available for multi-app data sharing
When to Use
| Use Case | Example |
|---|---|
| Operator desks | Staff calling queue numbers from a phone or tablet |
| Staff dashboards | Management consoles for monitoring operations |
| Public signup forms | Customer-facing registration or check-in |
| Public status boards | Real-time status displays accessible via shared link |
Configuration
Add the web mount point and enable SPA routing in telemetry.config.json:
{
"name": "queue-manager",
"version": "1.0.0",
"useSpaRouting": true,
"mountPoints": {
"render": "/render",
"settings": "/settings",
"web": "/queue-manager"
},
"devServer": {
"runCommand": "vite --port 3000",
"url": "http://localhost:3000"
}
}Web Path
The web mount point path must match the application name. For an application named queue-manager, the web path is /queue-manager.
useSpaRouting
Setting useSpaRouting to true is required when using the web mount point. This tells the platform to route all sub-paths back to the application so that client-side routing (React Router) handles navigation. Without it, refreshing or navigating directly to a sub-route like /queue-manager/l/Downtown returns a 404.
Store Scope Access
The web mount point runs in a standard browser with no device or instance context. Only two storage scopes are available:
| Scope | Available | Why |
|---|---|---|
application | ✅ Yes | Account-wide data (entity lists, global config) |
shared(namespace) | ✅ Yes | Entity-scoped shared data via dynamic namespaces |
instance | ❌ No | No device/instance context in browser |
device | ❌ No | No physical device context |
Attempting to use
store().instanceorstore().devicein a web view will fail. Use onlystore().applicationandstore().shared(namespace).
Default to Application Scope
Use application scope as your default in web views. Data written to application scope is readable by all mount points within the same app — Settings, Render, and Web — making it the natural choice for most web view data. Entity lists, configuration, and any data that your Render and Settings views also need should go here.
import { store } from '@telemetryos/sdk';
// Web view writes to application scope
await store().application.set('menuItems', updatedItems);
// Render view on the device can read the same data
const items = await store().application.get('menuItems');When to Use Shared Scope
Use shared(namespace) only when you need multiple different applications to share the same data. If your Web, Settings, and Render views are all part of the same application, application scope is sufficient and simpler.
The shared(namespace) scope is designed for scenarios where separately developed applications need to coordinate — for example, a weather app publishing temperature data that a separate energy dashboard app consumes.
Entity from URL, Not Instance Store
In the render mount point, the current entity (e.g., which location to display) is typically stored in instance scope. The web mount point has no instance scope, so the entity comes from URL parameters instead:
import { useParams } from 'react-router';
import { store } from '@telemetryos/sdk';
function WebEntityView() {
const { locationName } = useParams();
const decodedLocation = decodeURIComponent(locationName || '');
// Use application scope for data within the same app
const items = await store().application.get('menuItems');
// Only use shared scope if coordinating with other applications
const ns = `location::${decodedLocation.toLowerCase().replace(/\s+/g, '-')}`;
const data = await store().shared(ns).get('counters');
}When using shared namespaces, the namespace helper function should be shared between all views that access the same data to ensure they derive the same namespace string from a given entity name.
Routing
Web views use React Router with createBrowserRouter for client-side navigation.
App Name as Base Path
All web routes live under the application name. For queue-manager, all routes start with /queue-manager.
Entity-Driven URL Hierarchy
Structure URLs to mirror the data hierarchy. Each level of the URL corresponds to an entity selection:
/queue-manager → Root (list entities)
/queue-manager/l/:locationName → Entity detail
/queue-manager/l/:locationName/c/:counterId → Deep detail
React Router Setup
Define all mount point routes in a single router. Render and settings use their standard paths; web routes use the app name prefix:
import { createBrowserRouter, RouterProvider } from 'react-router';
import { Render } from './views/Render';
import { Settings } from './views/Settings';
import { WebLocations, WebCounters, WebCounter } from './views/Web';
const router = createBrowserRouter([
{ path: '/render', Component: Render },
{ path: '/settings', Component: Settings },
{ path: '/queue-manager', Component: WebLocations },
{ path: '/queue-manager/l/:locationName', Component: WebCounters },
{ path: '/queue-manager/l/:locationName/c/:counterId', Component: WebCounter },
]);
export function App() {
return <RouterProvider router={router} />;
}URL Encoding
Always encode entity names in URLs and decode them when reading parameters. Entity names may contain spaces, special characters, or unicode:
// Building a link
<Link to={`/queue-manager/l/${encodeURIComponent(locationName)}`}>
{locationName}
</Link>
// Reading a parameter
const { locationName } = useParams();
const decoded = decodeURIComponent(locationName || '');Styling
The html:has(.web) Pattern
html:has(.web) PatternThe TelemetryOS base template sets font-size: 1vmax on the <html> element for viewport-relative scaling on device displays. This scaling is not appropriate for web views, which run in standard browsers and should use standard font sizing.
Override the base scaling by applying a .web class to the root element of your web view components, then targeting it with html:has():
html:has(.web) {
font-size: 16px;
}export function WebLocations() {
return (
<div className="web">
{/* Web view content */}
</div>
);
}Every web view component should include className="web" on its outermost element. This ensures the html:has(.web) selector activates whenever any web view is rendered.
Design Guidelines
Web views use standard CSS without the viewport-relative scaling used by render views. Design for responsive layouts across phones, tablets, and desktops. Do not use useUiScaleToSetRem() or other SDK scaling utilities — these are for render mount points only.
Communication with Render and Settings
All three mount points share data through the application storage scope. This is the simplest and most common pattern:
Settings (admin portal)
→ Writes configuration to application scope
Web (browser)
→ Reads/writes data via application scope
Render (device)
→ Subscribes to application scope for real-time display
Settings manages the configuration, Web provides interactive access for staff or the public, and Render displays the output on physical devices. All three access the same underlying data through the application scope.
For advanced multi-app architectures where separately developed applications need to share data, use shared(namespace) to coordinate across application boundaries.
Next Steps
- Configuration - Complete
telemetry.config.jsonreference includinguseSpaRouting - Storage Methods - Storage scope details and API reference
- Mount Points - Overview of all mount point types
Updated about 2 hours ago