Planned specification

The bridge between your website and its app.

A small, versioned JavaScript SDK for coordinating page titles, navigation, badges, app lifecycle, sharing, notifications, and other native behavior—without a public backend service.

This page defines the intended implementation contract. The SDK, package, and CDN file are not available yet and should not be added to a production website.
app-bridge.js
const context = await SiteToApp.ready();

if (context.available) {
  await SiteToApp.page.setTitle('Your cart');
  await SiteToApp.navigation.setBadge('cart', 3);
}
No key, token, or public REST endpoint
Purpose

One website, with app-aware moments.

The SDK should be an optional progressive enhancement. Your pages still own content, routing, accounts, carts, and business rules. The native shell owns app chrome and device integrations. The SDK passes small, validated messages between the two.

There is no SiteTo.App public REST service in this design. Websites do not need a SiteTo.App key, and the SDK must never contain store credentials, customer secrets, or privileged account tokens.

Contextual page titles

Let a checkout, account, or article page update the native header while keeping the browser title correct.

Live navigation badges

Push a known value immediately or read a cart, inbox, or booking count from a same-origin URL.

Route-aware navigation

Keep the active native navigation item aligned with traditional pages and single-page application routes.

Lifecycle events

Refresh stale website state when the app resumes, reconnects, or opens from a notification or deep link.

Native actions

Use the system share sheet, permission prompts, settings, and subtle haptic feedback when supported.

Safe browser behavior

Every feature has a documented browser fallback or no-op so one website codebase continues to work everywhere.

Availability and setup

A future script, explicitly versioned.

The first release should support a small CDN script for ordinary websites and an npm package for bundled applications. Both must expose the same behavior and protocol version.

The following addresses are reserved examples for implementation planning. They do not host a working SDK today.
CDN scriptProposed v1
<!-- Planned CDN address — not live yet -->
<script
  src="https://cdn.siteto.app/sdk/v1/sitetoapp.min.js"
  defer
></script>
ES moduleProposed v1
// Planned package — not published yet
import { SiteToApp } from '@sitetoapp/web-sdk';

Major versions live in the URL and package contract. Non-breaking additions can ship within v1; renamed methods, changed payloads, or different fallback behavior require v2.

Readiness and context

Detect capabilities, not user agents.

SiteToApp.ready() begins a handshake with the native shell and resolves to an AppContext. It must also resolve in a normal browser, so application code never hangs while waiting for a bridge that is not present.

JavaScriptProposed v1
const context = await SiteToApp.ready({ timeout: 1500 });

if (!context.available) {
  // Normal browser: keep using your existing web behavior.
  return;
}

await SiteToApp.page.setTitle('Order details');
await SiteToApp.navigation.setActive('orders');
Context fieldValueReason
availablebooleanWhether a native shell completed a valid handshake.
platformios | android | webThe active runtime; never inferred from the user agent.
appVersionstring | nullThe installed native app version, when available.
sdkVersionstringThe loaded website SDK version.
protocolVersionnumber | nullThe bridge contract negotiated with the app.
capabilitiesstring[]Supported commands such as navigation.badge or share.
colorSchemelight | darkThe current native appearance.
safeAreatop, right, bottom, leftInsets in CSS pixels for custom full-screen layouts.
Feature code should check context.capabilities when support varies by installed app version. Platform checks are a last resort.
Page titles and state

Keep the native header in context.

A page can change the app header as customers move through products, articles, account sections, or checkout. By default, setTitle updates both the native header and document.title so browser history remains useful too.

JavaScriptProposed v1
// Update both the document title and the app header.
await SiteToApp.page.setTitle('Your cart');

// Change only the native header.
await SiteToApp.page.setTitle('Checkout', {
  document: false,
  native: true,
});

// Return to the title derived from the page or app configuration.
await SiteToApp.page.resetTitle();
page.setLoading(boolean)

Connect a website transition to the shell loading indicator. The app must still enforce an automatic timeout so a page cannot leave it running forever.

page.setPullToRefresh(boolean)

Disable pull-to-refresh for drawing surfaces, maps, or interactions where the gesture conflicts with page behavior.

Navigation badges

Show the count that matters now.

Navigation items use stable IDs configured in the SiteTo.App builder, such as cart, inbox, or bookings. Labels may change, but those IDs remain the contract between the website and app.

Set a known value

JavaScriptProposed v1
// Numbers are formatted by the native app: 100 becomes “99+”.
await SiteToApp.navigation.setBadge('cart', 3);

// Zero, null, or false hides the badge.
await SiteToApp.navigation.setBadge('cart', 0);

// Short status labels are also allowed.
await SiteToApp.navigation.setBadge('inbox', 'NEW');

Update from a URL

A watcher fetches in the website context—not from a separate SiteTo.App service. Relative, same-origin URLs are recommended because they keep normal site cookies and security rules intact.

Shopify-style cart exampleProposed v1
const cartBadge = SiteToApp.navigation.watchBadge('cart', {
  url: '/cart.js',
  jsonPath: 'item_count',
  interval: 30_000,
  refreshOn: ['ready', 'resume', 'focus', 'urlchange'],
  hideWhenZero: true,
  stale: 'keep',
});

// Refresh after a local add-to-cart action.
await cartBadge.refresh();

// Stop fetching when this page no longer owns the badge.
cartBadge.stop();
OptionDefaultBehavior
urlrequiredHTTPS or relative URL returning JSON or plain text.
jsonPathnoneDot path such as cart.item_count. Required when JSON is not itself the badge value.
intervaldisabledPolling interval in milliseconds, with a minimum of 15 seconds.
refreshOnready, resumeAny combination of ready, resume, focus, and urlchange.
credentialssame-originUses the browser Fetch credentials mode. Cross-origin requests still require CORS.
timeout5000Maximum fetch time in milliseconds.
hideWhenZerotrueHide the badge when the mapped result is 0, null, or false.
stalekeepKeep the last valid value after an error; clear is the alternative.
  • Numeric values display from 1–99; larger values use the native 99+ treatment.
  • Text values are trimmed, sanitized, and limited to four visible characters.
  • Polling pauses while the app is backgrounded and resumes with one immediate refresh.
  • Overlapping requests are cancelled; the newest valid response wins.
  • HTTP, parsing, and mapping failures emit sdk:error without breaking the page.
Native actions

Use device behavior where it earns its place.

Native actions should improve an existing web flow, not become a requirement for using the website. Permission prompts must follow a clear user action and explain their value in the page first.

Sharing, notifications, and hapticsProposed v1
await SiteToApp.share({
  title: 'Spring collection',
  text: 'These just arrived.',
  url: window.location.href,
});

// Must be called from a user click or tap.
const permission = await SiteToApp.notifications.requestPermission();

// Use sparingly for confirmation, not decoration.
await SiteToApp.haptics.impact('light');

Share with a useful fallback

Use the native share sheet in the app, navigator.share where available, then a clipboard fallback after a user action.

Keep permission status simple

Return default, granted, denied, or unsupported. Never expose a raw push token to page code.

Open external links deliberately

Internal approved URLs stay in the app. External HTTPS links open in the system browser unless app configuration says otherwise.

Rate-limit physical feedback

Haptics are ignored when unsupported and throttled natively to prevent a page from producing repeated disruptive feedback.

App events

Let the website respond to app lifecycle.

Events flow from the native shell into the SDK after schema and origin validation. Handlers receive public, minimal payloads and return an unsubscribe function so component-based sites can clean them up.

Event subscriptionsProposed v1
const unsubscribe = SiteToApp.on('app:resume', () => {
  cartBadge.refresh();
});

SiteToApp.on('navigation:reselect', ({ itemId }) => {
  if (itemId === 'home') window.scrollTo({ top: 0, behavior: 'smooth' });
});

SiteToApp.on('notification:opened', ({ url }) => {
  if (url) window.location.assign(url);
});

// Remove a listener when its owning component unmounts.
unsubscribe();
app:resume

The app returned to the foreground. Refresh time-sensitive page data and watched badges.

app:pause

The app moved to the background. Pause expensive work that is not already handled by the browser.

navigation:reselect

The user tapped the currently active native navigation item again. Commonly scrolls a feed to the top.

route:open

The shell received a trusted deep link that the page router should handle without a full reload.

notification:opened

The user opened a push notification. Payload contains an approved in-app URL and optional public metadata.

network:change

Connectivity changed between online and offline. The browser remains the source of truth for actual fetch success.

appearance:change

The native light or dark appearance changed. Payload includes the new color scheme.

sdk:error

A watcher, bridge command, or message validation failed. Intended for diagnostics, not sensitive logging.

Browser fallbacks

The website must remain a website.

Not being inside the app is a normal state, not an exception. The SDK must avoid console noise and return predictable results when its native counterpart is absent.

FeatureNormal browser behavior
readyResolves with available: false and platform: web after the timeout.
page.setTitleUpdates document.title when document is enabled; native delivery reports false.
badges and item stateDo not fetch or render by default. Return delivered: false without throwing.
navigation.openUses location.assign, location.replace, or window.open according to the target.
navigation.backUses history.back when browser history is available.
shareUses navigator.share, then clipboard after a user gesture.
notificationsReturns unsupported; it does not substitute the browser notification system.
hapticsSafe no-op with delivered: false.
Commands should resolve to a small result such as { delivered: false, reason: 'not-in-app' }. Invalid arguments and native denials use typed SiteToAppError codes.
Proposed v1 surface

Complete method reference.

SiteToApp.ready(options?)

Wait for the handshake, then resolve to the current AppContext. It always resolves; a normal browser returns available: false.

SiteToApp.getContext()

Return the last negotiated platform, versions, safe-area values, color scheme, and capability list.

SiteToApp.isAvailable()

Synchronously report whether a validated native bridge is currently connected.

SiteToApp.page.setTitle(title, options?)

Update document.title, the native header, or both. Empty titles are rejected.

SiteToApp.page.setLoading(loading)

Show or hide the shell loading indicator for a page transition or long-running action.

SiteToApp.page.setPullToRefresh(enabled)

Enable or disable native pull-to-refresh for screens where the gesture is appropriate.

SiteToApp.navigation.setBadge(itemId, value)

Set a configured navigation item badge. Zero, null, and false clear it.

SiteToApp.navigation.watchBadge(itemId, options)

Fetch and map a badge value on lifecycle triggers or a controlled interval. Returns refresh and stop controls.

SiteToApp.navigation.setActive(itemId)

Select a configured native navigation item without navigating the website.

SiteToApp.navigation.syncWithLocation(rules)

Observe browser history and keep the active item aligned with URL match rules. Returns an unsubscribe function.

SiteToApp.navigation.setItem(itemId, state)

Update the label, visibility, or enabled state of an existing configured item.

SiteToApp.navigation.open(url, options?)

Open an approved internal URL in the current web view or an external HTTPS URL in the system browser.

SiteToApp.navigation.back()

Ask the shell to go back, falling through to browser history when no native route is available.

SiteToApp.share(data)

Open the native share sheet, with navigator.share and clipboard fallbacks on the web.

SiteToApp.notifications.requestPermission()

Request notification permission after a user gesture and return the resulting status.

SiteToApp.notifications.openSettings()

Open the operating system settings for this app when permission was previously denied.

SiteToApp.haptics.impact(style)

Request light, medium, or heavy impact feedback. This is a no-op when unsupported.

SiteToApp.on(event, handler)

Subscribe to a validated app event and receive an unsubscribe function.

SiteToApp.once(event, handler)

Subscribe for the next matching event, then remove the handler automatically.

Initial typed errors: INVALID_ARGUMENT, UNSUPPORTED, NOT_ALLOWED, TIMEOUT, FETCH_FAILED, PROTOCOL_MISMATCH, and NATIVE_ERROR.

Bridge protocol

A small, acknowledged message envelope.

The JavaScript layer normalizes platform transport details. The first app implementation should use the React Native WebView transport and send serialized JSON through window.ReactNativeWebView.postMessage. The envelope stays independent of that transport so a future native WebView adapter can reuse it.

Website to app

Command envelopeProposed v1
{
  "channel": "sitetoapp",
  "protocolVersion": 1,
  "id": "sta_01J8ZQ6JY1",
  "type": "navigation.badge.set",
  "payload": {
    "itemId": "cart",
    "value": 3
  },
  "timestamp": 1789584000000
}

App acknowledgement

Response envelopeProposed v1
{
  "channel": "sitetoapp",
  "protocolVersion": 1,
  "type": "response",
  "replyTo": "sta_01J8ZQ6JY1",
  "ok": true,
  "payload": null
}

App to website

Validated native eventProposed v1
// Native shell → website. Internal SDK method; not public surface.
window.SiteToApp.__receive({
  channel: 'sitetoapp',
  protocolVersion: 1,
  type: 'event',
  event: 'app:resume',
  payload: { occurredAt: Date.now() },
});
  • The SDK starts with bridge.hello; the app responds with supported protocol versions and capabilities.
  • Every command gets a unique ID. Commands that need confirmation receive a response referencing that ID.
  • The default acknowledgement timeout is two seconds. Late responses are ignored after a timeout settles.
  • Messages are JSON only, limited to 64 KB, and parsed exactly once on each side.
  • Repeated title and badge changes are debounced; the latest valid value wins.
  • __receive is implementation-only, non-enumerable where possible, and rejects messages outside the schema.
Security rules

The bridge is narrow by design.

A WebView bridge creates native authority inside web content, so the shell must treat every page message as untrusted input—even when the page belongs to the customer.

1

Enable the bridge only on the exact HTTPS origins and URL patterns configured for that app. Disable it immediately before navigating elsewhere.

2

Allowlist message types and validate every payload against the negotiated protocol schema. Ignore unknown fields and reject unknown commands.

3

Never expose store credentials, push tokens, device identifiers, authentication cookies, or native filesystem paths to page JavaScript.

4

Accept only relative URLs or validated HTTPS destinations. Block javascript:, data:, file:, intent:, and custom schemes unless a specific command allowlists them.

5

Require a recent user gesture for permission prompts, sharing fallbacks, settings links, and disruptive native actions.

6

Apply per-command rate limits, a 64 KB message cap, safe string lengths, and native-side timeouts.

7

Redact payloads from production logs. Diagnostic errors may contain codes and message IDs, never customer content or secrets.

8

Keep watched badge requests inside browser Fetch rules. Same-origin credentials are the default; cross-origin URLs must pass CORS.

Implementation order

Ship the dependable core first.

The first implementation should stay deliberately small. Later capabilities only belong in the SDK after both app platforms can support the same contract and browser behavior is documented.

PhaseScopeCompletion test
1. BridgeHandshake, context, capability negotiation, acknowledgements, validation, typed errors.A normal browser and both app platforms resolve readiness consistently.
2. Core UITitles, direct badges, active navigation, URL matching, lifecycle events.SPA and multi-page test sites remain synchronized through reloads and resume.
3. Badge URLsFetch watcher, mapping, cancellation, background pause, error events.Shopify cart and generic JSON examples pass privacy, CORS, and stale-data tests.
4. Native actionsSharing, notification permission/settings, external URLs, haptics.User-gesture, denial, unsupported, and browser-fallback paths are covered.
5. ReleaseCDN bundle, npm package, TypeScript types, sample site, CSP and migration guidance.The documented snippets run unchanged against a production build.

Release checklist

  • Test iOS, Android, Safari, Chrome, slow networks, offline state, and app resume after a long background period.
  • Publish TypeScript definitions and keep the global script and module exports behaviorally identical.
  • Add an SDK compatibility matrix to each generated app build.
  • Replace every “planned” notice on this page only after the matching artifact is publicly available.

Have a use case this contract misses?

Share the website flow, the app behavior you need, and what should happen in a normal browser. That is enough to evaluate it before implementation.

Send a use case