Swan SDK
Getting Started

Web

Install, initialize, and verify your first event on Web.

Modern browsers (Chrome 90+ · Firefox 90+ · Safari 14+ · Edge 90+) · TypeScript-first · Tree-shakeable ESM + CJS bundles · Drop-in <script> tag · Zero runtime dependencies

Install

Option A — npm (bundled apps)

yarn add @loyalytics/swan-web-sdk
# or
npm install @loyalytics/swan-web-sdk
import { Swan, SwanEvents } from '@loyalytics/swan-web-sdk';

ESM and CJS entry points both ship; modern bundlers (Vite, webpack, Rollup, esbuild, Next.js) pick the right one automatically.

Option B — <script> tag (no build step)

Drop the SDK onto any HTML page. Swan and SwanEvents become available on window:

<script src="https://cdn.swan.cx/web-sdk/v1.1.1/swan.min.js"></script>
<script>
  Swan.initialize('your-app-id', { production: true });
  Swan.identify('user-123', { email: 'jane@example.com' });
  SwanEvents.productViewed({ sku: 'ABC-100', price: 49.95 });
</script>

Each version path on cdn.swan.cx is immutable and served with a long-lived cache header (max-age=31536000, immutable), so production traffic gets a single hot-cache entry per release. Pin to an exact version — bumping the SDK is a one-line edit of the version segment in the URL.

If your CSP doesn't allow cdn.swan.cx or you'd rather keep the SDK fully inside your origin, download dist/swan.min.js from the npm package and serve it yourself:

<script src="/assets/swan-web-sdk-1.1.1.min.js"></script>

Initialize

The minimal initialize call:

import { Swan } from '@loyalytics/swan-web-sdk';

Swan.initialize('your-app-id', { production: true });

Swan.addInitializedListener(() => {
  console.log('Swan ready:', Swan.getSwanIdentifier());
});

initialize returns synchronously. Subscribe to addInitializedListener to be notified when device registration completes in the background.

Configuration options

Swan.initialize(appId, config) accepts a typed SwanConfig object. All fields are optional with sensible defaults.

Swan.initialize('your-app-id', {
  // ─── Environment ────────────────────────────────────────────────
  production: true,           // Default: true. Set to false for dev/staging.
  debug: false,               // Default: false. Set to true for verbose SDK logs.

  // ─── Web Push subsystem gate ────────────────────────────────────
  pushNotifications: {
    enabled: true,            // Default: true (opt-out). Set to false to
                              // disable the Web Push subsystem entirely —
                              // `Swan.subscribeToPush()` returns false
                              // immediately without registering the SW
                              // or prompting.
  },

  // ─── Location-tagging gate ──────────────────────────────────────
  location: {
    enabled: false,           // Default: false (opt-in). Set to true if
                              // your host app will call
                              // `Swan.updateLocation(lat, lng)` with
                              // coordinates. `Swan.isLocationEnabled()`
                              // reflects this flag.
  },

  // ─── Advanced / escape hatches ──────────────────────────────────
  // Tenant VAPID public key — normally NOT needed. The SDK reads it
  // from the device-register response. Pass this only if your backend
  // hasn't yet served the field, or for test rigs.
  vapidPublicKey: undefined,

  // Service Worker path — defaults to '/swan-service-worker.js'.
  // Override only if you host the SW at a different path.
  serviceWorkerUrl: '/swan-service-worker.js',
});

The shape mirrors the iOS and Android SDKs' SwanConfig blocks so cross-platform integrators see the same surface.

Identify + track

Same wire shape as the other platforms:

Swan.identify('user-123', { email: 'jane@example.com' });
Swan.track('productViewed', { sku: 'ABC-100', price: 49.95 });
SwanEvents.productViewed({ sku: 'ABC-100', price: 49.95 });

Push setup (Web Push)

The Web SDK uses the standard Web Push API. The tenant VAPID public key is served by the Swan backend on device-register — your host app does not need to configure it.

Serving the Service Worker

The SDK ships a bundled Service Worker that the SDK registers on subscribeToPush(). You need to serve it at /swan-service-worker.js (or a custom path you pass via SwanConfig.serviceWorkerUrl). Pick the recipe that matches your build:

Next.js / static-asset frameworks — copy the bundled SW into your public/ directory at build time. Add this to package.json scripts:

{
  "scripts": {
    "prebuild": "cp node_modules/@loyalytics/swan-web-sdk/dist/sw.js public/swan-service-worker.js"
  }
}

Vite — same recipe, into public/ so Vite serves it at the root.

webpack — use CopyWebpackPlugin to emit dist/sw.js to your output directory, or run the cp step in a pre-build hook.

Plain HTML / no build step — copy the file once and check it into your static assets:

cp node_modules/@loyalytics/swan-web-sdk/dist/sw.js public/swan-service-worker.js

The Service Worker MUST be served from the root of your origin (or scoped via the Service-Worker-Allowed response header) so it can intercept push events for the whole site.

Subscribe on user interaction

const subscribed = await Swan.subscribeToPush();
// → true on success; false if permission denied / browser doesn't support push

Handle taps in your host page:

Swan.addNotificationOpenedListener(({ route, keyValuePairs }) => {
  if (route) router.push(route);
});

Subscription idempotency contract

Swan.subscribeToPush() is idempotent and safe to call on every page load, but doing so is wasteful — the SDK re-POSTs the same subscription to the backend each time. The recommended pattern is to gate on isPushSubscribed():

if (await Swan.isPushSubscribed()) {
  // User already opted in. Backend has the subscription from the
  // original subscribeToPush() call.
} else {
  // Show "Enable notifications" CTA. Call Swan.subscribeToPush()
  // on user click — that's when the browser shows the permission prompt.
}

If you have reason to believe the backend's stored copy of the subscription is stale (e.g., a backend restore from snapshot) even though the browser still has a live subscription, call Swan.resyncSubscription(). It re-POSTs the current subscription without re-prompting or re-subscribing. Returns true on success, false if no subscription exists or the POST failed.

Per-browser web-push coverage

BrowserWeb Push subscribeWeb Push deliveryNotes
Chrome 90+ (desktop + Android)FCM-backed
Edge 90+Same pipeline as Chrome
Firefox 90+ (desktop + Android)Mozilla autopush
Safari macOS 13+APNs-based Web Push
iOS / iPadOS Safari 16.4+ (PWA only)⚠️⚠️Only when the site is installed as a PWA (Add to Home Screen). Standalone Safari tabs cannot receive push
iOS / iPadOS Safari < 16.4Not supported. Swan.subscribeToPush() returns false
Samsung Internet 14+Chromium-based
In-app WebViews (Instagram / Facebook / TikTok)Push not supported by spec
Android WebViews (any embedded WebView component)Push not supported. Use the native Android SDK in the host app instead

Environment requirements

  • HTTPS required. Web Push only works on origins served over HTTPS. http://localhost is treated as secure by browsers for development, but every other host MUST use TLS or Swan.subscribeToPush() will fail.
  • Incognito / Private Browsing is unsupported. pushManager.subscribe() either throws or returns a subscription the gateway immediately invalidates depending on the browser. The SDK degrades gracefully — Swan.subscribeToPush() returns false — but don't expect Web Push to function in Private Browsing.
  • Service Workers required. Web Push depends on a Service Worker registered on the host origin. See Serving the Service Worker above.

Notification rendering features

Web Push notifications support a subset of the Web Notifications API per browser. The SDK passes through whatever fields the backend sends; rendering is the browser's responsibility.

FeatureChromeEdgeFirefoxSafari
Title + body
Icon
Banner image
Click → deep link
Action buttons (CTAs)⚠️ desktop only⚠️ desktop only
requireInteraction flag✅ desktop✅ desktop✅ desktop only
Sound override
Carousel (swipeable images)

Fields the browser doesn't support are silently ignored — the notification still renders, just without that feature. If you need richer rendering on a specific browser, host apps can intercept via the bundled Service Worker before display.

Next steps

On this page