npmjs.com ↗
Installsignalbird/app · /react · /vue · /angular · /react-native

App - your own chat interface and mobile push

This surface is for your customer's user: a visitor, or a user of your app. It does live chat and push device registration.

You have two options:

The ready-made widget The App SDK
Install one <script> a package plus your own interface
Appearance managed from the panel entirely yours
Mobile none iOS, Android, React Native
When chat on a site, quickly chat embedded inside the product

For the ready-made widget see the Widget document. This page describes the SDK.

The key

Open an app in the panel with Inbox → Settings → Apps (or with createApp via the SDK). The sb_public_live_… key you are given is public and is embedded in the client.

Its safety comes from restriction, not from secrecy:

  • it works only from allowed origins (web),
  • it touches only the visitor's own data,
  • it cannot send anything and cannot read a contact list.

Do not put your team key (sb_…) in the client. The constructor catches that and errors; and even if it did not, a key once published belongs to everyone.

The visitor secret and storage

The identity has two parts: the app key and the visitor secret. The secret is returned only in the startSession response and storing it is mandatory: if it does not survive on the device, the user loses their chat history every time they open the app.

Platform Default How to change it
Web localStorage the storage option
React Native none - you must supply one asyncStorageAdapter(AsyncStorage)
iOS (Swift) UserDefaults a SignalbirdStorage adapter
Android (Kotlin) memory (tests only) a SharedPreferences wrapper

The stored record carries the app key too. If you rotate the key, the old identity is discarded automatically and a new session opens.


React / Next.js

bash
npm install signalbird
tsx
import { SignalbirdProvider, useChat, useIdentify } from 'signalbird/react'

export function App() {
  return (
    <SignalbirdProvider publicKey={process.env.NEXT_PUBLIC_SIGNALBIRD_APP_KEY!}>
      <Chat />
    </SignalbirdProvider>
  )
}

function Chat() {
  const {
    enabled,          // is chat on for this app (the bootstrap answer)
    loading,
    messages,
    unread,
    agentTyping,
    withinHours,      // false outside working hours
    send,
    typing,
    markRead,
    close,
  } = useChat({ open: true })

  // When the user signs in, bind the visitor to the contact record
  useIdentify(user && { external_id: user.id, email: user.email, name: user.name })

  if (!enabled) return null

  return (
    <>
      {!withinHours && <OfflineBanner />}
      {messages.map((m) => <Bubble key={m.id} message={m} />)}
      {agentTyping && <Typing />}
      <Composer onSend={send} onType={() => typing(true)} />
    </>
  )
}

open changes only the polling rate; the session is not rebuilt every time the panel is opened. For a badge there is a shortcut that works without the chat interface being mounted:

tsx
import { useUnreadCount } from 'signalbird/react'

const unread = useUnreadCount()

Push registration:

tsx
import { usePushRegistration } from 'signalbird/react'

usePushRegistration(token ? { token, platform: 'web', provider: 'webpush' } : null)

Vue 3

ts
import { signalbirdPlugin, useChat } from 'signalbird/vue'

app.use(signalbirdPlugin, { publicKey: import.meta.env.VITE_SIGNALBIRD_APP_KEY })
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useChat } from 'signalbird/vue'

const isOpen = ref(false)
const { state, send, typing, markRead } = useChat({ open: isOpen })
</script>

<template>
  <div v-for="m in state.messages" :key="m.id">{{ m.body }}</div>
  <span v-if="state.agentTyping">typing…</span>
  <input @input="typing(true)" @keyup.enter="send(($event.target as HTMLInputElement).value)" />
</template>

Polling stops automatically when the component is unmounted.


Angular

ts
// main.ts
import { provideSignalbird } from 'signalbird/angular'

bootstrapApplication(App, {
  providers: [provideSignalbird({ publicKey: environment.signalbirdAppKey })],
})
ts
// chat.component.ts
import { SignalbirdService } from 'signalbird/angular'

@Component({
  template: `
    <div *ngFor="let m of (chat$ | async)?.messages">{{ m.body }}</div>
    <input #box (keyup.enter)="send(box.value)" />
  `,
})
export class ChatComponent implements OnInit {
  private signalbird = inject(SignalbirdService)

  chat$ = this.signalbird.chat$()

  ngOnInit() { this.signalbird.setOpen(true) }

  send(body: string) { this.signalbird.send(body) }
}

The package carries no compile-time dependency on Angular (it uses no decorators), so upgrading your Angular version does not mean waiting for a new release.


React Native / Expo

tsx
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AppState } from 'react-native'
import {
  createSignalbirdApp,
  asyncStorageAdapter,
  useNativeChat,
  useNativePush,
} from 'signalbird/react-native'

// Supplying the storage is MANDATORY
const client = createSignalbirdApp({
  publicKey: 'sb_public_live_…',
  storage: asyncStorageAdapter(AsyncStorage),
})

export function ChatScreen() {
  const [foreground, setForeground] = useState(true)

  useEffect(
    () => AppState.addEventListener('change', (s) => setForeground(s === 'active')).remove,
    [],
  )

  const { messages, send, agentTyping } = useNativeChat(client, {
    open: true,
    isForeground: foreground,   // in the background the polling round is skipped: battery
  })

  useNativePush(client, token ? { token, platform: 'ios' } : null)

  return <MessageList data={messages} typing={agentTyping} onSend={send} />
}

Obtaining the push token (Expo Notifications or Firebase Messaging) and showing the permission dialog is the app's job: when you ask for that permission is a product decision, and the store rules take it seriously.


Swift (iOS)

Swift Package Manager: https://github.com/Pariette-Inc/signalbird.sdk

swift
import Signalbird

let client = try SignalbirdApp(config: .init(
    publicKey: "sb_public_live_…",
    chatKey: "destek",
    pushKey: "mobil"
))

// Is chat on, which colour, are we inside working hours
let boot = try await client.bootstrap()

try await client.startSession(["name": "Ayşe", "email": "ayse@example.com"])
try await client.startConversation(body: "Where is my parcel?")

// New messages: with a cursor, so the whole list is not downloaded again
let updates = try await client.getConversation(conversationID, after: lastMessageID)

try await client.setTyping(conversationID: conversationID, isTyping: true)
try await client.markRead(conversationID: conversationID, lastMessageID: lastID)

// APNs
try await client.registerDevice(token: apnsToken, externalID: user.id, identityHash: user.signalbirdHash)

If you want Keychain, supply your own storage:

swift
struct KeychainStorage: SignalbirdStorage {
    func get(_ key: String) -> String? { … }
    func set(_ key: String, _ value: String) { … }
    func remove(_ key: String) { … }
}

let client = try SignalbirdApp(config: .init(
    publicKey: "sb_public_live_…",
    chatKey: "destek",
    pushKey: "mobil",
    storage: KeychainStorage()
))

Kotlin (Android)

kotlin
implementation("io.signalbird:signalbird-sdk:1.9.0")
kotlin
import io.signalbird.sdk.*

class PrefsStorage(private val prefs: SharedPreferences) : SignalbirdStorage {
    override fun get(key: String) = prefs.getString(key, null)
    override fun set(key: String, value: String) = prefs.edit().putString(key, value).apply()
    override fun remove(key: String) = prefs.edit().remove(key).apply()
}

val client = SignalbirdApp(SignalbirdAppConfig(
    publicKey = "sb_public_live_…",
    chatKey = "destek",
    pushKey = "mobil",
    storage = PrefsStorage(prefs),
))

lifecycleScope.launch {
    client.bootstrap()
    client.startSession(mapOf("name" to "Ayşe"))
    client.startConversation("Where is my parcel?")

    val updates = client.getConversation(conversationId, after = lastMessageId)

    client.registerDevice(token = fcmToken, externalId = user.id, identityHash = user.signalbirdHash)
}

Every method is suspend and runs on the IO dispatcher.


Without a framework (plain TypeScript)

typescript
import { SignalbirdApp, ChatSession } from 'signalbird/app'

const client = new SignalbirdApp({ publicKey: 'sb_public_live_…' })

// The raw endpoints
await client.bootstrap()
await client.startSession({ name: 'Ayşe' })
await client.startConversation({ body: 'Hello' })

// or the state layer
const session = new ChatSession(client, { active: true })

session.subscribe((state) => render(state))
await session.start()
await session.send('Where is my parcel?')

ChatSession takes on: optimistic sending (the message lands in the list right away and is replaced by the server's answer), the polling ladder, the unread count and the typing state.

How polling works

There is no WebSocket. Cursor-based polling recovers by itself when the connection drops and does not burn battery on a mobile network.

State Interval
Panel open 3 s
Panel closed - first 3 rounds 20 s
the next 2 rounds 60 s
after that 180 s
Tab/app in the background the round is skipped

The ladder resets when new data arrives: waiting three minutes for the next poll just as a conversation comes back to life would leave the user feeling abandoned.

Identity verification (identity_hash)

The app key is public; anyone could put someone else's id into external_id. Signalbird therefore treats external_id (and email) as trusted only when a valid identity_hash comes with it (2.7.0). Without the hash:

  • the visitor is not linked to your contact record,
  • the device is not bound to that user as a push target,
  • the channel agent's tools see the identity only under visitor.unverified.

Generate the hash on your own server with the secret key and hand it to the app with the signed-in user (e.g. in your login response):

text
identity_hash = hex( HMAC-SHA256( hex(SHA-256(sb_secret_live_…)), external_id ) )

Server helpers: PHP Signalbird::identityHash($id), Node client.identityHash(id), Python client.identity_hash(id), Go client.IdentityHash(id), .NET client.IdentityHash(id).

typescript
await client.startSession({ external_id: user.id, identity_hash: user.signalbirdHash })
await client.identify({ external_id: user.id, identity_hash: user.signalbirdHash, email: user.email })
await client.registerDevice({ token, platform: 'web', external_id: user.id, identity_hash: user.signalbirdHash })
// `identityHash` is accepted too; it is sent as `identity_hash`.

In Swift and Kotlin add "identity_hash" to the dictionary/map input; registerDevice(…, identityHash:) takes it as a parameter.

The visitor object carries two fields: identity_verified (the hash was valid) and verified (captcha passed). Captcha exists only in the hosted web widget; mobile requests without an Origin are exempt and this SDK sends no token. The captcha field in the bootstrap response is informational.

Version notice

The SDK reports its version on every request: X-Signalbird-Sdk: app/<version> (react-native/<version> in React Native). When a newer version is released the API responds with Signalbird-Sdk-Status: outdated (or unsupported once it is no longer supported) and the SDK writes a warning once per process (console.warn). The warning is not an error: the request result does not change.

In the panel, Domains → [domain] → Keys shows which SDK version each key is used with; keys on an old version also get a banner at the top of the panel and a notification. To update:

bash
npm i signalbird@latest

Error behaviour

No method throws an exception (apart from the key check in the constructor). The result is always the {ok, status, code?, message?} envelope.

Code Meaning
VISITOR_INVALID The secret is invalid: the local identity is cleared and the next call opens a new session
CHAT_UNAVAILABLE The quota is full or chat is off
ORIGIN_NOT_ALLOWED This domain is not defined on the app
MODULE_DISABLED Your package does not include the chat or push module
NETWORK_ERROR / TIMEOUT Network (status 0)

The full method list (17 methods) and the rules of behaviour: signalbird.sdk/docs/CONTRACT.md § 11.