Manual Development

Write TelemetryOS applications yourself — editor, hot module replacement, mock SDK, and debugging

Manual Development

This is the traditional workflow: you write the code, the Developer App runs it, and you iterate with hot module replacement on the simulated device canvas.

Prefer to direct an AI agent instead? See AI Agent Development. The two workflows share the same SDK, templates, and publish pipeline — pick the one you'll spend most of your time in, and you can always combine them.

Before You Start

Complete Getting Started first so you have the Developer App installed and a project created. This page picks up from a project open in the Developer App with the canvas rendering.

The Workflow

  1. Open the project in your editor. Use Tools → Open in [Editor] (Cmd+E) to launch your configured editor pointed at the project directory. VS Code, Cursor, Zed, WebStorm, Vim, Emacs — anything that edits files works.
  2. Edit src/views/Render.tsx or src/views/Settings.tsx. Save. The canvas updates live via hot module replacement — no reload required.
  3. Exercise both mount points together. Change a value in Settings, confirm it flows to Render (the store subscription does this for you). Cycle aspect ratios (Cmd+] / Cmd+[) and backgrounds (Cmd+Shift+] / Cmd+Shift+[) to verify responsive layouts.
  4. Reset when you need a clean slate. Tools → Delete Stub Data (Cmd+D) wipes locally persisted mock storage, media, and account data so the app behaves as if launched for the first time.
  5. Restart on stuck HMR or config changes. Tools → Restart Dev Server (Cmd+Alt+R) stops and restarts the process defined in devServer.runCommand.

Dev Server Configuration

The Developer App reads devServer from telemetry.config.json to decide how to run your local dev server:

{
  "devServer": {
    "runCommand": "npm install && vite --port $PORT",
    "url": "http://localhost:$PORT",
    "readyPattern": "VITE.*ready in"
  }
}
  • $PORT is replaced at runtime with an available port, avoiding conflicts when multiple projects are open.
  • readyPattern is a regex matched against the dev server's stdout to detect readiness. If omitted, the Developer App polls url instead.
  • runCommand can target any bundler — Vite, Webpack, Next.js, Parcel, etc. See Configuration for examples.

Mock SDK Behavior

Every SDK method returns mock data during local development, so applications build and test without a live platform connection.

  • Storage is backed by file-based persistence in the project's .telemetryos/ directory. Values survive dev-server restarts; store().instance.subscribe() fires on cross-view changes so Settings→Render data flow matches device behavior.
  • Media (media().getAllByTag, getById, etc.) returns sample MediaContent objects.
  • Playlist & overrides — operations succeed and return true but don't affect actual playback.
  • Account & user methods return placeholder data with mock identifiers.
  • Proxy (proxy().fetch(url)) forwards the request through the Developer App's HTTP proxy, so CORS and authentication behavior match production.
  • Weather & MQTT return representative sample data you can edit in .telemetryos/ to test different scenarios.

Storage

await store().instance.set('key', 'value');
const value = await store().instance.get('key');

store().instance.subscribe('key', (value) => {
  console.log('Value changed:', value);
});

Media

const items = await media().getAllByTag('featured');
// items contains mock MediaContent objects

Proxy

// Use proxy().fetch() for all external APIs — avoids CORS and matches device behavior
const res = await proxy().fetch('https://api.example.com/data');
const data = await res.json();

Playlist & Overrides

await playlist().nextPage();           // returns true
await overrides().setOverride('alert'); // returns true

Accounts & Users

const account = await accounts().getCurrent();
// account = { id: 'mock-account-id', ... }

Debugging

Browser DevTools

Right-click the canvas and choose Inspect, or use Tools → Toggle Developer Tools.

In the Console tab:

// Access SDK directly in the console
telemetry.store().instance.get('key')
telemetry.playlist().nextPage()

In the Application tab, view file-based storage contents and service workers. Use the Network tab to monitor proxy().fetch() calls and verify request headers, query strings, and response bodies.

React DevTools

Install the React DevTools browser extension to inspect component trees, view props and state, profile renders, and debug hooks.

Common Pitfalls

"SDK is not configured"

Call configure() before any other SDK method. This goes in src/index.tsx before React renders:

import { configure } from '@telemetryos/sdk';

configure();

Mount point blank or 404

Check that routing in src/App.tsx matches the paths in telemetry.config.json > mountPoints:

const path = window.location.pathname;
if (path === '/settings') return <Settings />;
if (path === '/render') return <Render />;

Hot reload not working

Use Tools → Restart Dev Server (Cmd+Alt+R). If the problem persists, check for TypeScript compilation errors blocking the rebuild, and clear the cache with Tools → Delete Stub Data if you're suspicious of corrupt local state.

Storage not persisting

Each storage scope has different visibility. store().device is only available in Render mount points on real devices; in the Developer App, prefer store().instance (for Settings↔Render sync) or store().application (for account-wide data).

// Persists across reloads — visible to Settings and Render
await store().instance.set('key', 'value');

// Not available in Settings or Web mount points
await store().device.set('key', 'value');

CORS error fetching external APIs

Use proxy().fetch() instead of fetch() for external domains. The proxy forwards requests server-side and avoids CORS restrictions.

Testing Patterns

Exercise both mount points together. From Settings, save values and verify the UI reflects them. Then check that Render subscribes to the same values and updates when Settings change. Cover empty, loading, error, and populated states.

When iterating on responsive layouts, cycle through every aspect ratio preset — 5:1 Chiron and 1:5 Skyscraper are easy to forget and notoriously easy to break.

Building for Production

Production builds are driven by npm run build, which runs tsc && vite build.

npm run build

Creates production files in dist/:

dist/
├── index.html
├── assets/
│   ├── index-abc123.js
│   └── index-def456.css
└── telemetry.config.json

telemetry.config.json must be in the build output — the platform reads it on upload. The public/ directory's contents are copied to dist/ automatically by Vite.

Archive for Manual Upload

File → Archive (Cmd+Shift+A) bumps the CalVer patch version, runs npm run build, and writes a .tar.gz into the project's out/ directory. Upload this archive to Studio's Applications interface for manual deployment.

To re-archive the current version without bumping, use File → Archive without Version Bump (Alt+Cmd+Shift+A).

Best Practices

Test locally before every commit and exercise both mount points. Use TypeScript to catch mistakes at compile time.

Keep API keys and other secrets out of source control. Use .env.local with Vite's import.meta.env:

const API_KEY = import.meta.env.VITE_API_KEY;
# .env.local
VITE_API_KEY=your-api-key-here

Cover edge cases: no configuration set, error states, loading states, and combinations of data. On-screen applications run unattended for long periods — a rare bug shown for hours is a real problem.

Instrument performance where it matters:

console.time('data-fetch');
const data = await fetchData();
console.timeEnd('data-fetch');

Next Steps