Available · v1

The bridge between your website and its app.

A small, versioned JavaScript SDK for coordinating page titles, navigation, badges, app lifecycle, sharing, and notifications. It needs no key and no public backend service.

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 is 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 involved. 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.

Automatic active tab

The app highlights the right tab from the current URL by itself, on traditional pages and single-page applications.

Lifecycle events

Refresh stale website state when the app resumes, or respond when a notification is opened.

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

One script, explicitly versioned.

Add the CDN script to an ordinary website, or install the npm package in a bundled application. Both expose the same behavior and protocol version.

CDN scriptv1
<script
  src="https://siteto.app/sdk/v1/sitetoapp.min.js"
  defer
></script>
ES modulev1
npm install @sitetoapp/web-sdk

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.

JavaScriptv1
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.setBadge('/orders', 2);
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.
capabilitiesstring[]Supported commands such as navigation.badge or share.
Feature code should check context.capabilities, because support varies by platform. Platform checks are a last resort.

What works where

FeatureiOSAndroid
Titles, loading state, pull-to-refreshYesYes
Badges, item label and visibilityAll menus (header icons show no label)All menus (header icons show no label)
Active itemBottom tabs, automatic from the URLBottom tabs, automatic from the URL
Open, back, handleOpen, notification eventsYesYes
Share sheet, notification settings linkYesYes
Lifecycle eventsYesYes
notifications.requestPermissionYesYes, on Android 13 and later
haptics.impactYesYes
context.appVersionYesYes
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. setTitle updates both the header and document.title, so browser history remains useful too.

JavaScriptv1
// Updates the app's header and document.title together.
await SiteToApp.page.setTitle('Your cart');

// Return to your app's name.
await SiteToApp.page.resetTitle();
page.setLoading(boolean)

Connect a website transition to the app’s loading screen. It hides itself after eight seconds, 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.

A menu item is addressed by the path of its link, so the tab linking to your cart page is '/cart'. Your website already knows its paths, and changing a label or translating it changes nothing. The same link in two menus stays in step. Badges appear on bottom tabs, side menu items, and header icons, and a path no menu links to is rejected with NOT_ALLOWED.

Set a known value

JavaScriptv1
// A menu item is addressed by the path of its link.
// Numbers are formatted by the 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 examplev1
const cartBadge = SiteToApp.navigation.watchBadge('/cart', {
  url: '/cart.js',
  select: (cart) => cart.item_count,
  interval: 30_000,
});

// 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.
selectnoneFunction that picks the badge value from the response. Without it, the response itself is the value.
intervaldisabledPolling interval in milliseconds, with a minimum of 15 seconds. It always refreshes on load and when the app resumes.
credentialssame-originUses the browser Fetch credentials mode. Cross-origin requests still require CORS.
  • 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.
  • After a failed request the last valid badge stays up, and sdk:error fires without breaking the page. Requests time out after five seconds.
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 hapticsv1
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 and navigator.share where available. Otherwise it reports delivered: false, so you can offer your own.

Keep permission status simple

Returns granted, denied, or unsupported. The app also asks once at startup, so a person who declined there needs openSettings(). Notifications start the next time the app opens after they say yes. Push tokens are never exposed 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 so a page cannot produce repeated disruptive feedback.

App events

Let the website respond to app lifecycle.

Events flow from the app into the SDK after validation. on returns an unsubscribe function so component-based sites can clean up. For connectivity and color scheme, use the browser’s own online events and prefers-color-scheme, which work in the app too.

Event subscriptionsv1
const unsubscribe = SiteToApp.on('app:resume', () => {
  refreshPrices();
});

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

// The app opens the notification's page itself. Use this for analytics.
SiteToApp.on('notification:opened', ({ url }) => {
  analytics.track('push_opened', { 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 active bottom tab again while on its page. Commonly scrolls a feed to the top.

notification:opened

The user opened a push notification while the app was running in the background. The payload contains its URL, which the app opens itself. It does not fire when a notification launches a closed app.

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 right away with available: false and platform: web.
page.setTitleUpdates document.title; 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 where available.
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.
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, and capability list.

SiteToApp.page.setTitle(title)

Update the app header and document.title together. Empty titles are rejected.

SiteToApp.page.resetTitle()

Return the app header to the app name.

SiteToApp.page.setLoading(loading)

Show or hide the app’s loading screen for a page transition or long-running action. It hides itself after eight seconds.

SiteToApp.page.setPullToRefresh(enabled)

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

SiteToApp.navigation.setBadge(path, value)

Set a badge on the menu items linking to a path. Zero, null, and false clear it.

SiteToApp.navigation.watchBadge(path, options)

Fetch a badge value on load, on app resume, and optionally on an interval. Returns refresh and stop controls.

SiteToApp.navigation.setActive(path)

Override the highlighted tab until the next page load. Pass null to return to automatic.

SiteToApp.navigation.setItem(path, state)

Update the label, visibility, or enabled state of the menu items linking to a path.

SiteToApp.navigation.open(url, options?)

Open a page on your website in the app, or an external HTTPS URL in the system browser.

SiteToApp.navigation.back()

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

SiteToApp.navigation.handleOpen(handler)

Let your router handle tab and notification changes instead of the page reloading. Returns a stop function.

SiteToApp.share(data)

Open the native share sheet, with a navigator.share fallback 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 an app event and receive an unsubscribe function.

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

Bridge protocol

A small, acknowledged message envelope.

In the app, messages travel over the React Native WebView transport as serialized JSON through window.ReactNativeWebView.postMessage. The envelope is independent of that transport.

Website to app

Command envelopev1
{
  "channel": "sitetoapp",
  "protocolVersion": 1,
  "id": "sta_01J8ZQ6JY1",
  "type": "navigation.badge.set",
  "payload": {
    "path": "/cart",
    "value": 3
  },
  "timestamp": 1789584000000
}

App acknowledgement

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

App to website

Validated native eventv1
// 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.
  • Each page starts unconnected: the app sends it no events until it says hello.
  • 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 app shell treats every page message as untrusted input—even when the page belongs to you. It enforces these rules:

1

The bridge only answers pages on your app’s website origin, with or without www. Messages from any other page are dropped, and replies check the origin before they run.

2

Message types are allowlisted and every payload is validated against the negotiated protocol schema. Unknown fields are ignored and unknown commands rejected.

3

Store credentials, push tokens, device identifiers, authentication cookies, and native filesystem paths are never exposed to page JavaScript.

4

Only relative URLs or validated HTTPS destinations are accepted. javascript:, data:, file:, intent:, and custom schemes are blocked unless a specific command allowlists them.

5

The SDK requires a recent user gesture for permission prompts and settings links. The app also limits how often a page can ask.

6

Commands are rate-limited, messages are capped at 64 KB, and strings and native calls have length limits and timeouts.

7

The bridge does not log payloads. Errors returned to the page contain a code and a short message, never internal details.

8

Watched badge requests follow browser Fetch rules. Same-origin credentials are the default; cross-origin URLs must pass CORS.

Before you go live

Test your integration.

The SDK only changes behavior inside your app. Check both paths so app users get the native experience and browser visitors keep the website working exactly as before.

  • Open the pages that call the SDK in a normal mobile browser and confirm nothing breaks when context.available is false.
  • Test in the app on iOS and Android, including slow networks, offline state, and resuming after a long time in the background.
  • Check titles, badges, and the highlighted tab after reloads and client-side route changes.
  • For native actions, test the user-gesture, permission-denied, and unsupported paths.
  • If your site uses a Content Security Policy, allow the SDK script origin.

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 for us to evaluate it.

Send a use case