Offline
Understanding how TelemetryOS applications run locally on devices with graceful offline handling
Offline
TelemetryOS applications are designed for reliability in real-world deployment environments where internet connectivity cannot be guaranteed. Understanding the build-deploy-run architecture is essential for creating applications that gracefully handle network conditions.
Build & Deploy Architecture
Server-Side Build Process
When code pushes to a Git repository, TelemetryOS performs a complete build on the server:
Developer pushes code → GitHub repository
↓
TelemetryOS build servers compile the application
↓
Build artifacts (HTML, CSS, JS, assets) created
↓
Packaged application distributed to devices
↓
Devices download and cache locally
The build process produces all the artifacts needed for the application to run independently on a device. This includes JavaScript bundles (transpiled and minified), CSS stylesheets (processed and optimized), HTML entry points, static assets such as images, fonts, and media files, and source maps for debugging.
TelemetryOS respects your existing build tooling throughout this process. Vite, Webpack, Parcel, and Rollup configurations are all honored. Dependencies from npm, yarn, or pnpm are installed, custom build scripts are executed, and environment variables are injected into the final output.
Device Download & Local Storage
Once built, the application is downloaded to devices and stored locally:
Device filesystem:
/telemetryos/applications/example-app/
├── index.html
├── assets/
│ ├── main.js (bundled application code)
│ ├── styles.css
│ └── images/
└── manifest.json
Every device receives a complete local copy of all application files. These files are versioned, so multiple versions can be cached for rollback if needed. The local copy persists across reboots, meaning files survive device restarts without re-downloading. When a new version becomes available, the device automatically downloads the update.
Local Execution Model
Application Runs Entirely from Device
Application code is served from local storage, not fetched from the internet:
Device powers on
↓
TelemetryOS Player loads
↓
Playlist schedules the application
↓
Application loads from local filesystem
↓
HTML/CSS/JS served locally (FAST!)
↓
Application renders in Chrome iframe
What this means:
- ✅ Application code loads instantly (no network latency)
- ✅ The application starts and runs without internet
- ✅ All bundled assets available immediately
- ✅ No concerns about CDN availability or DNS failures
What Works Offline
These features are fully available without internet connectivity.
Your entire application core runs locally: HTML rendering and layout, CSS styling and animations, all JavaScript execution and logic, and any framework operations from React, Vue, or others. Event handlers, user interaction, and graphics through Canvas, WebGL, or SVG all function without a network connection.
The TelemetryOS SDK APIs also work fully offline. This includes the Storage API across all scopes, the Platform API for device, user, and account information, the Playlist API for navigation and current page data, and the Media API for accessing the TelemetryOS media library.
Standard browser APIs and locally cached data are equally available. You can use LocalStorage, SessionStorage, IndexedDB, the Cache API with Service Workers, Web Workers, and Canvas or Audio/Video playback. Any data you have previously stored through the Storage API, IndexedDB databases, LocalStorage values, or Service Worker caches remains accessible regardless of connectivity.
What Requires Internet
These operations require network connectivity.
Any call to an external service needs an active internet connection. This covers REST API requests via fetch() or axios, GraphQL queries, WebSocket connections, and Server-Sent Events. Third-party service integrations such as weather APIs, payment gateways, CRM/ERP systems, social media feeds, and real-time data sources all depend on network availability.
Cloud-hosted resources that were not bundled with your application also require connectivity. External images and videos, CDN-hosted libraries, remote fonts, and streaming media will fail to load when the device is offline.
Finally, certain TelemetryOS cloud services need internet access to function: storage synchronization to the cloud, remote device management, screenshot uploads, and log aggregation all communicate with TelemetryOS servers and cannot operate in isolation.
Graceful Degradation Patterns
Applications must handle network failures gracefully. Users should never see broken interfaces or cryptic errors when internet connectivity is lost.
Detecting Connectivity
Use browser APIs to detect online/offline state:
// Check current connectivity status
if (navigator.onLine) {
console.log('Device is online');
} else {
console.log('Device is offline');
}
// Listen for connectivity changes
window.addEventListener('online', () => {
console.log('Connection restored');
syncPendingData();
hideOfflineBanner();
});
window.addEventListener('offline', () => {
console.log('Connection lost');
showOfflineBanner();
pauseDataSync();
});Important: navigator.onLine detects network interface status, not actual internet connectivity. A device can report "online" but still fail to reach external services.
Handling Failed Network Requests
Always wrap external API calls with error handling:
async function fetchWeatherData(city) {
try {
const response = await fetch(
`https://api.weather.com/v1/current?city=${city}`,
{ timeout: 5000 } // Set reasonable timeout
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Cache successful response
await cacheWeatherData(city, data);
return data;
} catch (error) {
console.warn('Weather fetch failed:', error);
// Fall back to cached data
const cached = await getCachedWeatherData(city);
if (cached) {
return cached;
}
// Return safe default
return {
city,
temperature: '--',
conditions: 'Data unavailable',
offline: true
};
}
}Good error handling follows a consistent set of principles. Always catch network errors, since fetch() throws on network failures. Set timeouts so your application does not wait indefinitely for a response. Check the HTTP status with response.ok to verify success before consuming the body. When a request fails, fall back to previously cached data so the user still sees something useful. Provide safe defaults to ensure the UI is never left in a broken state, and show offline indicators so users understand when they are viewing stale or unavailable data.
UI Patterns for Offline State
Offline Banner
function OfflineBanner() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (isOnline) return null;
return (
<div className="offline-banner">
⚠️ No internet connection. Displaying cached data.
</div>
);
}Stale Data Indicators
function DataDisplay({ data, lastUpdated }) {
const isStale = Date.now() - lastUpdated > 60000; // 1 minute
return (
<div>
<h2>{data.title}</h2>
{isStale && (
<span className="stale-indicator">
Last updated: {formatTimestamp(lastUpdated)}
</span>
)}
<div>{data.content}</div>
</div>
);
}Retry Mechanisms
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
} catch (error) {
if (attempt === maxRetries) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Storage & Caching Strategies
TelemetryOS Storage API
The Storage API provides intelligent caching with automatic synchronization:
import { configure, store } from '@telemetryos/sdk';
configure();
// Store data - automatically cached on device
await store().device.set('weatherData', {
city: 'New York',
temperature: 72,
timestamp: Date.now()
});
// Retrieve data - works offline
const cached = await store().device.get('weatherData');Storage scope characteristics:
| Scope | Offline Access | Sync to Cloud | Use Case |
|---|---|---|---|
| device | ✅ Yes | ❌ No | Device-specific cache |
| instance | ✅ Yes | ✅ Yes | Configuration values |
| application | ✅ Yes | ✅ Yes | Shared resources |
| shared(ns) | ✅ Yes | ✅ Yes | Inter-app communication |
Key benefits:
- Data persists across application restarts
- Synchronizes to cloud when online
- Conflicts resolved automatically
- No explicit "save" or "sync" calls needed
IndexedDB for Large Datasets
For large datasets, use IndexedDB directly:
// Open database
const db = await openDB('weather-cache', 1, {
upgrade(db) {
db.createObjectStore('forecasts', { keyPath: 'city' });
}
});
// Store data offline
await db.put('forecasts', {
city: 'New York',
forecast: [/* 7-day forecast */],
timestamp: Date.now()
});
// Retrieve cached data
const cached = await db.get('forecasts', 'New York');
if (cached && Date.now() - cached.timestamp < 3600000) {
// Use cached data (less than 1 hour old)
displayForecast(cached.forecast);
} else {
// Fetch fresh data
try {
const fresh = await fetchForecast('New York');
await db.put('forecasts', {
city: 'New York',
forecast: fresh,
timestamp: Date.now()
});
displayForecast(fresh);
} catch (error) {
// Fall back to stale cache
if (cached) {
displayForecast(cached.forecast, { stale: true });
}
}
}Service Workers for Advanced Caching
Implement Service Workers for request-level caching:
// service-worker.js
const CACHE_NAME = 'weather-app-v1';
const CACHE_URLS = [
'/',
'/index.html',
'/main.js',
'/styles.css'
];
// Cache on install
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(CACHE_URLS);
})
);
});
// Serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response; // Cache hit
}
// Clone the request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
// Cache successful responses
if (response && response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
}).catch(() => {
// Network failed, return offline page
return caches.match('/offline.html');
});
})
);
});Best Practices
1. Cache Aggressively
Store data locally whenever possible:
async function loadData() {
// Try cache first (instant)
const cached = await store().device.get('data');
if (cached) {
displayData(cached);
}
// Then fetch fresh data in background
try {
const fresh = await fetchFromAPI();
await store().device.set('data', fresh);
displayData(fresh);
} catch (error) {
// Already showing cached data, log error silently
console.error('Background fetch failed:', error);
}
}2. Set Reasonable Timeouts
Don't wait indefinitely for network requests:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.warn('Request timeout');
}
throw error;
}3. Implement Queue-Based Sync
Queue operations when offline, sync when online:
const pendingQueue = [];
async function saveData(data) {
// Always save locally first
await store().device.set(data.id, data);
// Try to sync to server
if (navigator.onLine) {
try {
await syncToServer(data);
} catch (error) {
pendingQueue.push(data);
}
} else {
pendingQueue.push(data);
}
}
// Sync queue when online
window.addEventListener('online', async () => {
while (pendingQueue.length > 0) {
const data = pendingQueue.shift();
try {
await syncToServer(data);
} catch (error) {
// Re-queue on failure
pendingQueue.unshift(data);
break;
}
}
});4. Show Data Freshness
Always indicate when data might be stale:
function DataTimestamp({ timestamp }) {
const age = Date.now() - timestamp;
const minutes = Math.floor(age / 60000);
let indicator;
if (minutes < 5) {
indicator = '🟢 Live';
} else if (minutes < 60) {
indicator = `🟡 ${minutes}m ago`;
} else {
indicator = `🔴 ${Math.floor(minutes / 60)}h ago`;
}
return <span className="freshness">{indicator}</span>;
}5. Test Offline Scenarios
Always test applications in offline mode:
Chrome DevTools:
Offline testing is available through DevTools (F12) > Network tab > "Offline" throttling mode. This simulates network disconnection for verifying application behavior.
Automated Testing:
// Cypress test
it('handles offline mode gracefully', () => {
cy.visit('/');
// Go offline
cy.window().then(win => {
win.dispatchEvent(new Event('offline'));
});
// Verify offline banner appears
cy.contains('No internet connection').should('be.visible');
// Verify cached data still displays
cy.get('[data-testid="weather-data"]').should('exist');
});6. Bundle Critical Assets
Don't rely on CDNs for critical resources:
// ❌ Bad - CDN dependency
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
// ✅ Good - bundled dependency
import library from 'library'; // Bundled via npm7. Provide Offline Fallbacks
Design UI components with offline states:
function WeatherDisplay() {
const [weather, setWeather] = useState(null);
const [isOffline, setIsOffline] = useState(!navigator.onLine);
if (isOffline && !weather) {
return (
<div className="offline-state">
<Icon name="cloud-off" />
<h3>Unable to load weather data</h3>
<p>Check the internet connection and try again.</p>
</div>
);
}
return (
<div className="weather-data">
{isOffline && <OfflineBadge />}
{/* Weather data display */}
</div>
);
}Common Patterns
Cache-First Strategy
Load from cache immediately, update in background:
async function loadWithCacheFirst(key, fetchFn) {
// 1. Load from cache immediately
const cached = await store().device.get(key);
if (cached) {
displayData(cached.data);
}
// 2. Fetch fresh data in background
try {
const fresh = await fetchFn();
await store().device.set(key, {
data: fresh,
timestamp: Date.now()
});
displayData(fresh);
} catch (error) {
// Keep showing cached data
if (!cached) {
showErrorState();
}
}
}Network-First Strategy
Try network first, fall back to cache:
async function loadWithNetworkFirst(key, fetchFn) {
try {
// 1. Try network first
const fresh = await fetchFn();
await store().device.set(key, {
data: fresh,
timestamp: Date.now()
});
return fresh;
} catch (error) {
// 2. Fall back to cache
console.warn('Network failed, using cache:', error);
const cached = await store().device.get(key);
if (cached) {
return cached.data;
}
throw new Error('No data available');
}
}Stale-While-Revalidate
Show stale data, update when fresh data arrives:
async function loadWithSWR(key, fetchFn, maxAge = 300000) {
// 1. Load cached data
const cached = await store().device.get(key);
const isStale = !cached || (Date.now() - cached.timestamp > maxAge);
// 2. Display cached data immediately if available
if (cached) {
displayData(cached.data, { stale: isStale });
}
// 3. Revalidate if stale
if (isStale) {
try {
const fresh = await fetchFn();
await store().device.set(key, {
data: fresh,
timestamp: Date.now()
});
displayData(fresh, { stale: false });
} catch (error) {
// Keep showing cached data
console.error('Revalidation failed:', error);
}
}
}Deployment Considerations
Application Updates
When code changes are pushed, TelemetryOS builds the new version on its servers and distributes it to devices. Each device downloads the update when it is online, and the update is applied the next time the application restarts. The old version remains cached on the device so it can be used as a rollback if anything goes wrong.
Updates happen automatically in the background without interrupting the current session. The new version loads the next time the application starts, and no user action is required.
Version Rollback
If an update causes issues, rollback to previous version:
Studio → Applications → [App Name] → Versions → Rollback
Rollback is instant - devices immediately revert to previous working version.
Testing Updates
Before deploying an update across your entire fleet, validate it in stages. Start with a branch deployment to a set of test devices, then use a canary deployment to roll it out to a small subset of production devices. Monitor the logs in Studio for errors during the rollout, and use screenshot verification for visual confirmation that the application is rendering correctly.
Troubleshooting
Application Won't Load Offline
Symptoms: Application shows "Unable to load" when internet is disconnected.
Causes:
- External dependencies not bundled (CDN resources)
- Service Worker misconfiguration
- HTML/CSS/JS not properly cached
Solutions:
- Bundle all dependencies:
npm install library(not CDN links) - Verify build output includes all assets
- Check browser console for failed resource loads
- Test with DevTools offline mode before deployment
Data Not Persisting
Symptoms: Data disappears when application restarts.
Causes:
- Using SessionStorage instead of LocalStorage
- Not using Storage API properly
- Clearing cache on restart
Solutions:
- Use Storage API:
store().device.set(key, value) - Verify data is saved:
const data = await store().device.get(key) - Check browser storage limits (IndexedDB: ~50-100MB typical)
Slow Application Performance
Symptoms: Application loads slowly even with internet connection.
Causes:
- Large bundle sizes
- Too many external API calls on startup
- Blocking network requests
Solutions:
- Optimize bundle size (code splitting, tree shaking)
- Load critical data first, defer non-critical requests
- Use async/await, avoid blocking operations
- Implement loading states for better perceived performance
Key Takeaways
TelemetryOS applications run entirely from device storage. All HTML, CSS, and JavaScript is served locally, so no internet connection is required for core functionality. The build process happens on TelemetryOS servers -- when you push code, the platform compiles your application and distributes it to devices automatically.
While the application itself runs offline, any external network requests still require internet connectivity. This includes calls to third-party APIs, cloud-hosted resources that are not bundled with the application, and TelemetryOS cloud services like storage synchronization and log aggregation. Graceful degradation is therefore essential: applications should handle network failures cleanly, fall back to cached data, and clearly indicate offline state to users.
The TelemetryOS Storage API is your primary tool for intelligent caching, providing automatic synchronization when online and reliable local access when offline. Pair this with sensible retry and queue mechanisms to sync data once connectivity is restored, bundle all critical dependencies so you never rely on CDNs for essential resources, and test offline scenarios thoroughly using DevTools before deploying to production devices.
Next Steps
- Deploying — Register and deploy applications through Studio
- Storage Methods — Complete storage methods reference
- Local Development — Test applications locally before deployment
- Code Examples — Complete offline handling patterns
Updated about 1 hour ago