JavaScript
Ensure you have completed the Prerequisites section before continuing.
Installation
If you're using npm:
npm install @dashx/browser
If you're using yarn:
yarn add @dashx/browser
Configuration
DashX needs to be configured as early as possible in your application:
import DashX from '@dashx/browser'
DashX.configure({
publicKey: '...', // required
targetEnvironment: '...', // required
baseUri: '...', // optional (defaults to https://api.dashx.com/graphql)
realtimeBaseUri: '...', // optional (defaults to wss://realtime.dashx.com)
})
Your Public Key is not sensitive and can be safely included in client-side code. If you use multiple Environments, we recommend using environment variables or build-time configuration to set the targetEnvironment.
Multiple Instances
If you need separate clients (e.g., for different environments or workspaces), use createClient:
import DashX from '@dashx/browser'
const staging = DashX.createClient({
publicKey: '...',
targetEnvironment: 'staging',
})
const production = DashX.createClient({
publicKey: '...',
targetEnvironment: 'production',
})
staging.track('Event A')
production.track('Event B')
createClient returns an independent Client instance. The singleton methods (DashX.track(), etc.) only work with the instance created by DashX.configure().
Usage
All asynchronous methods return Promises and can be used with async/await.
User Management
setIdentity)You can set uid without token and still use public features/resources (for example identify, track, and public content). The token is the DashX Identity Token: a JWT from your backend, signed with your workspace private key (see User management). Setting token grants access to your private resources in DashX—use it only for users who need that access, and treat it as sensitive. Pass null for token (or omit it) when that access is not needed. Both uid and token are optional (string | null).
// User id only — public features/resources (add token for your private resources)
DashX.setIdentity('user-123')
// Signed-in user + JWT for private resources
DashX.setIdentity('user-123', 'your-identity-token')
// After token refresh, persist the new token (same uid)
DashX.setIdentity('user-123', 'refreshed-identity-token')
// Send user attributes
await DashX.identify({
uid: 'user-123',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
})
// Or identify with just a UID
await DashX.identify('user-123')
// Clear identity, generate new anonymous ID, and clean up push state
DashX.reset()
Analytics
DashX.track('Button Clicked', {
label: 'Click here',
placement: 'top'
})
Messaging
Web push notifications require your app to be configured for Firebase Cloud Messaging (FCM). Before proceeding, ensure you have:
- A Firebase project with Cloud Messaging enabled
- Your VAPID key from Firebase Console > Project Settings > Cloud Messaging > Web Push certificates
- A service worker file for handling background notifications (see Background Notifications below)
Push Notifications
import { initializeApp } from 'firebase/app'
import { getMessaging, getToken, deleteToken, onMessage } from 'firebase/messaging'
const firebaseApp = initializeApp({ /* your Firebase config */ })
const fbMessaging = getMessaging(firebaseApp)
// Wrap Firebase's function-based API into the interface DashX expects
const messaging = {
getToken: (options) => getToken(fbMessaging, options),
onMessage: (handler) => onMessage(fbMessaging, handler),
deleteToken: () => deleteToken(fbMessaging),
}
// Subscribe to push notifications
await DashX.subscribe(messaging, { vapidKey: 'your-vapid-key' })
// Unsubscribe from push notifications. Resolves with `{ success: boolean }`:
// - true → backend found and unsubscribed a matching contact.
// - false → no matching contact (anonymous UID rotated, FCM token stale,
// contact already unsubscribed, or this device never subscribed
// in the current session). Device ends up unsubscribed in
// either case; the boolean is for diagnostics.
// The promise rejects on transport / SDK-state failures (Firebase
// `deleteToken` failure, GraphQL or network errors).
const { success } = await DashX.unsubscribe()
// Fire-and-forget also still works:
DashX.unsubscribe()
You can optionally pass a serviceWorkerRegistration if you manage your own service worker:
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js')
await DashX.subscribe(messaging, {
vapidKey: 'your-vapid-key',
serviceWorkerRegistration: registration,
})
subscribe() automatically requests notification permission if not already granted.
Foreground Notifications
When a push arrives while the app tab is focused, the SDK does two things:
- Shows the system notification banner automatically — same title/body/icon/URL as the background path.
- Fires your
onPushNotificationReceivedcallback so you can layer custom in-app UI (in-app toast, unread-count bump, analytics, etc.) on top of or instead of the banner.
const unsubscribe = DashX.onPushNotificationReceived((payload) => {
console.log(payload)
// payload: { id, title, body, image, url }
})
// Later, to stop listening:
unsubscribe()
If you render your own in-app UI and don't want the system banner duplicating it, opt out:
await DashX.subscribe(messaging, {
vapidKey: 'your-vapid-key',
showForegroundNotifications: false,
})
Tap handling (tracking + URL navigation) continues to flow through the service worker's click handler regardless of how the banner was shown.
registration.showNotification(...) — the call that draws the system banner from a focused tab — requires an active ServiceWorkerRegistration. Firebase's getToken() inside subscribe() registers the SW lazily on the first call, so until the user clicks your "Enable notifications" button, no SW is active and the foreground banner can't render (the onPushNotificationReceived callback still fires). Pass registerServiceWorker to have the SDK register the SW at app mount. See Registering the service worker below.
Wiring the foreground listener without re-calling subscribe()
Firebase's messaging.onMessage listener is registered in the current page's JS scope. On page reload, it dies — so if you only call subscribe() behind an "Enable notifications" button, foreground pushes will silently disappear after a reload (the service-worker background path still works). Wire the listener at app mount to keep foreground pushes flowing:
// On app mount, after DashX.configure(...)
DashX.onPushNotificationReceived((payload) => {
// Your in-app handler
})
DashX.attachForegroundMessaging(messaging)
// ... elsewhere, behind your permission-prompting UI
<button onClick={() => DashX.subscribe(messaging, { vapidKey: '...' })}>
Enable notifications
</button>
attachForegroundMessaging(messaging) does not prompt for permission, fetch a token, or register with DashX — it just wires the foreground listener. Idempotent and safe to call on every mount.
The Web Notifications API's sound option is ignored by Chrome and Safari, so there's no portable way to deliver a custom sound URL through the system banner. If you need a branded sound effect, include a custom field in your push data payload and play it from the focused tab inside your callback:
DashX.onPushNotificationReceived((payload) => {
if (payload.soundUrl) {
new Audio(payload.soundUrl).play().catch(() => { /* blocked without gesture */ })
}
})
This only works while the tab is focused (Firebase foreground path). Background service-worker audio playback is not supported by the platform.
Registering the service worker
Pass registerServiceWorker (path to your SW file) to either attachForegroundMessaging or subscribe. The SDK calls navigator.serviceWorker.register(path) internally, caches the registration, and reuses it for both Firebase's getToken and for rendering the foreground banner. Idiomatic at app bootstrap:
// On app mount, after DashX.configure(...)
DashX.onPushNotificationReceived((payload) => { /* your in-app handler */ })
DashX.attachForegroundMessaging(messaging, {
registerServiceWorker: '/firebase-messaging-sw.js',
})
// ... elsewhere, behind your permission-prompting UI — subscribe reuses the
// registration the SDK already cached above, so no double-register.
<button onClick={() => DashX.subscribe(messaging, { vapidKey: '...' })}>
Enable notifications
</button>
Alternatives supported on both methods:
serviceWorkerRegistration— pass an already-registered registration object. Takes precedence overregisterServiceWorker. Useful when another tool in your build (e.g. a Vite plugin, Workbox setup) registers the SW itself.- Omit both — the SDK falls back to
navigator.serviceWorker.ready. Fine for apps that register the SW elsewhere at bootstrap, but note that.readystays pending indefinitely until something registers an SW — so omitting both fields means the foreground banner can't render on the first push if the SW hasn't been registered yet.
subscribe()'s existing vapidKey / tag / showForegroundNotifications fields continue to work alongside registerServiceWorker.
Background Notifications (Service Worker)
Create a firebase-messaging-sw.js file in your public directory to handle notifications when the app is in the background:
/* @dashx/browser vX.Y.Z — bump this on every SDK update */
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js')
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js')
// Copy from node_modules/@dashx/browser/dist/sw-helper.umd.js to your public directory
importScripts('./dashx-sw-helper.umd.js')
// Take over immediately on update — without these, a new SW sits in "waiting"
// state until every tab of the origin closes. See "Updating the service worker
// when the SDK changes" below for the full reasoning.
self.addEventListener('install', (event) => { event.waitUntil(self.skipWaiting()) })
self.addEventListener('activate', (event) => { event.waitUntil(self.clients.claim()) })
firebase.initializeApp({
// your Firebase config
})
const messaging = firebase.messaging()
const dashx = createDashXServiceWorkerHandler({
publicKey: 'your-public-key',
targetEnvironment: 'production',
})
messaging.onBackgroundMessage((payload) => dashx.onBackgroundMessage(payload, self.registration))
self.addEventListener('notificationclick', (event) => dashx.onNotificationClick(event, self.clients))
self.addEventListener('notificationclose', (event) => dashx.onNotificationClose(event))
The service worker helper automatically:
- Displays a browser notification with the title, body, and image from the DashX payload
- Tracks DELIVERED when the notification arrives
- Tracks CLICKED when the user clicks the notification (and opens the URL if provided)
- Tracks DISMISSED when the user dismisses the notification
If you prefer to handle background messages manually without the helper:
messaging.onBackgroundMessage((payload) => {
const data = JSON.parse(payload.data.dashx)
self.registration.showNotification(data.title, {
body: data.body,
icon: data.image,
data: { url: data.url },
})
})
Notification tracking (DELIVERED, CLICKED, DISMISSED) is handled automatically — by the service worker helper for background notifications, and by onPushNotificationReceived for foreground notifications.
Updating the service worker when the SDK changes
Service workers are notoriously sticky. The three pieces in the example above — the version comment, the skipWaiting + clients.claim listeners, and a build-time copy of dashx-sw-helper.umd.js — are what make npm install @dashx/browser@<new> actually reach end users without a manual SW unregister.
Why the version comment. The browser decides whether to register a new SW by byte-comparing the top-level SW file against the currently-installed version. dashx-sw-helper.umd.js is loaded via importScripts, so the browser does not watch it directly — but it does refetch imported scripts whenever the top-level SW byte-changes. Embedding the SDK version as a comment ensures the top-level file's bytes change on every update, which in turn forces a fresh fetch of the helper.
If you generate this SW at build time (recommended — see below), splice the version in from your installed @dashx/browser's package.json so it updates automatically.
Why skipWaiting + clients.claim. By default, a new SW enters the "waiting" state and only activates once every tab of the origin closes. Most users never fully close your app tab, so a new SW can sit in waiting for hours or days. The two listeners activate it on install and take over already-open tabs on activate — combined with the version-comment byte-bump, end users get the new SW on their very next page load.
Why re-copy the helper on every build. public/dashx-sw-helper.umd.js is a static asset — it isn't served from node_modules. A dependency-lock bump on @dashx/browser doesn't refresh it on its own. Either copy it as part of your build step, or automate it in your build tool. A Vite example that also splices the version into the SW comment:
import { readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
name: 'sync-dashx-sw',
buildStart() {
const dashxPkg = JSON.parse(
readFileSync(resolve(__dirname, 'node_modules/@dashx/browser/package.json'), 'utf8'),
)
// Refresh the helper bytes from the installed SDK version
writeFileSync(
resolve(__dirname, 'public/dashx-sw-helper.umd.js'),
readFileSync(resolve(__dirname, 'node_modules/@dashx/browser/dist/sw-helper.umd.js')),
)
// Splice the version into the top-level SW file so its bytes change
const swPath = resolve(__dirname, 'public/firebase-messaging-sw.js')
const sw = readFileSync(swPath, 'utf8').replace(
/\/\* @dashx\/browser v[^\s]+/,
`/* @dashx/browser v${dashxPkg.version}`,
)
writeFileSync(swPath, sw)
},
},
],
})
With the comment, the listeners, and the build-time copy in place, updating the SDK becomes an ordinary npm install — no manual SW unregister dance for you or your users.
Even with all three pieces wired correctly, a handful of edge cases can stretch update latency:
- Tab kept open for days. Browsers check for an SW update on navigation and at most every 24h on a long-lived tab. A user with the app open for a week may see the old SW until they reload.
- CDN/proxy caching the SW file. Modern browsers force
Cache-Control: no-cacheon the top-level SW request, but a misconfigured CDN that ignores or overrides this can serve a stalefirebase-messaging-sw.js. Verify your CDN doesn't add longmax-ageto that path. - Build pipeline doesn't rebuild on dependency bump.
npm installalone doesn't regenerate the SW — your CI must run the build step that re-splices the version comment and copies the helper. If you skip the build, the old SW ships.
In practice, the version comment + skipWaiting + clients.claim + build-time copy gets the new SW to the vast majority of users on their next reload. The remainder pick it up within 24h. There is no portable mechanism that beats those bounds; they're the platform's, not the SDK's.
Development tips
- Keep DevTools → Application → Service Workers → "Update on reload" ticked during active development. Every hard-reload then picks up SW changes even if you haven't byte-bumped.
- When a new SW seems stuck, Unregister the current one, close all tabs of the origin, reopen a fresh tab. Full reset.
chrome://serviceworker-internals/shows the full SW registration state across all origins if you need to debug which worker is controlling what.
In-App Notifications
For real-time in-app notifications via WebSocket:
// Connect to the real-time WebSocket
DashX.connectWebSocket()
// Fetch existing notifications (populates cache)
await DashX.fetchInAppNotifications()
// Watch for notification list updates in real-time
const unwatch = DashX.watchFetchInAppNotifications((notifications) => {
console.log(notifications) // Array of { id, sentAt, readAt, renderedContent }
})
// Watch unread count
const unwatchCount = DashX.watchFetchInAppNotificationsAggregate((count) => {
console.log('Unread:', count)
})
// Register a callback for incoming notifications
const off = DashX.onNotification((notification) => {
console.log('New notification:', notification)
})
// Clean up
unwatch()
unwatchCount()
off()
DashX.disconnectWebSocket()
User Preferences
// Stored preferences need an identified user: call setIdentity with uid + JWT when private resources require it (see User Management above)
const preferences = await DashX.fetchStoredPreferences()
// Save preferences
await DashX.saveStoredPreferences({ push: true, email: false })
Contact Management
// Fetch user's contact channels
const contacts = await DashX.fetchContacts()
// Save contacts
await DashX.saveContacts([
{ kind: 'EMAIL', value: 'john@example.com' },
{ kind: 'PHONE', value: '+1234567890', tag: 'PRIMARY' },
])
CMS
const record = await DashX.fetchRecord('blog/welcome', {
language: 'en_US',
preview: true,
})
fetchRecord accepts the following optional arguments:
| Name | Type | Example |
|---|---|---|
preview | boolean | true |
language | string | "en_US" |
fields | object[] | |
include | object[] | |
exclude | object[] |
const records = await DashX.searchRecords('blog', {
filter: { status: 'published' },
order: [{ created_at: 'DESC' }],
limit: 10,
language: 'en_US',
preview: true,
})
searchRecords also supports a fluent builder pattern:
const records = await DashX.searchRecords('blog')
.filter({ status: 'published' })
.order({ created_at: 'DESC' })
.limit(10)
.language('en_US')
.preview(true)
.all()
searchRecords accepts the following optional arguments:
| Name | Type | Example |
|---|---|---|
filter | object | { status: 'published' } |
order | object[] | [{ created_at: 'DESC' }] |
limit | number | 10 |
page | number | 1 |
preview | boolean | true |
language | string | "en_US" |
fields | object[] | |
include | object[] | |
exclude | object[] |
Assets
const asset = await DashX.upload({
file: fileInput.files[0], // a File object
resource: 'users',
attribute: 'avatar',
})
console.log(asset.url)
Asset uploads are automatically polled for completion (5 retries, 3 s interval).
Cart
// Add item to cart
const cart = await DashX.addItemToCart({
itemId: 'item-uuid',
pricingId: 'pricing-uuid',
quantity: '1',
reset: false,
})
// Apply a coupon
await DashX.applyCouponToCart({ couponCode: 'SAVE20' })
// Remove a coupon
await DashX.removeCouponFromCart({ couponCode: 'SAVE20' })
// Fetch cart
const cart = await DashX.fetchCart({})
// Transfer anonymous cart to identified user
await DashX.transferCart({})
AI Agent
// Load agent configuration
const agent = await DashX.loadAiAgent({ publicEmbedKey: 'your-embed-key' })
// Invoke agent
const response = await DashX.invokeAiAgent({
publicEmbedKey: 'your-embed-key',
prompt: 'How can I reset my password?',
conversationId: 'optional-conversation-id', // for multi-turn conversations
})