Alaznah

Install peers, permissions, tokens, and place your first Alaznah 1:1 call.

Quick Start

End-to-end path from a blank React Native app to a 1:1 voice or video call with the built-in UI (CallingScreen).

This page is meant to work on its own. Deeper references: Installation · Permissions · Authentication · Call UI.

Before you start

RequirementDetail
React Native0.76+, Android compileSdk 35 or 36Compatibility
App typeRN CLI or Expo development build (not Expo Go)
ConsoleProject in the Developer Console
DevicesTwo devices (or device + simulator where your setup allows)
IdentitiesEach device uses a different userId (e.g. alice and bob)

Physical devices are strongly preferred for real mic/camera tests.


1. Install the SDK

Peers stay in your app (not bundled inside @alaznah/calling). Pick your package manager:

npm install @alaznah/calling

# Required peers
npm install react-native-webrtc react-native-incall-manager @react-native-community/netinfo react-native-svg
ManagerNotes
npmInstall @alaznah/calling, then run the Required peers command (npm tab only)
Yarn / pnpmOne command — package + peers together

REQUIRED peers: react-native-webrtc, react-native-incall-manager, @react-native-community/netinfo, react-native-svg

OPTIONAL: react-native-callkeep — only if you explicitly choose CallKeep. The SDK’s own native calling path does not require it.

Without react-native-webrtc, the first startCall fails with WebRTC adapters not found.

Package: @alaznah/calling on npm. Full install notes: Installation.


2. Native permissions

iOS — Info.plist

Edit ios/<AppName>/Info.plist (or Xcode → Info):

xml
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for audio and video calls.</string>

<!-- Required for video calls -->
<key>NSCameraUsageDescription</key>
<string>Camera access is required for video calls.</string>

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
  <string>voip</string>
  <string>remote-notification</string>
</array>

Then:

bash
cd ios && pod install && cd ..

Android — AndroidManifest.xml

Inside <manifest> (before <application>):

xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<!-- Video (or apps that offer both voice and video) -->
<uses-permission android:name="android.permission.CAMERA" />

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />

Voice-only vs video matrices: Permissions.

Expo (development build)

Expo Go is not supported — you need a development build (or bare workflow after prebuild).

In app.json / app.config:

json
{
  "expo": {
    "plugins": ["@alaznah/calling"]
  }
}

Then generate native projects (or use EAS):

bash
npx expo prebuild
# or
eas build --profile development

After prebuild, confirm the generated Info.plist and AndroidManifest.xml still include the keys/permissions above (edit if the plugin did not inject them). Full Expo notes: Installation.

Rebuild

Metro reload is not enough after install or plist/manifest changes:

bash
# React Native CLI
npx react-native run-ios
npx react-native run-android

# Expo development build
npx expo run:ios
npx expo run:android

3. Get a calling token

The SDK needs a short-lived Alaznah calling JWT. That is not your app’s login cookie.

Two tokens (read this once)

NameSourceUsed by
sessionTokenYour login (Supabase / Firebase / your JWT / SSO session). Alaznah does not issue this.App → your backend Authorization: Bearer …
Calling tokenMinted by your backend (or Console Test Token for demos)Returned from getAuthToken() → SDK → signaling

Full walkthrough (diagram, sample backend route, mint HTTP): Authentication.

Option A — Console Test Token (fastest for this guide)

No backend yet:

  1. Open Console → Tokens (or Tokens in the developer console).
  2. Select your project.
  3. Enter userId (e.g. alice on device A, bob on device B).
  4. Generate → Copy token.

Paste it into getAuthToken for local demos only (tokens expire — mint again when they do).

Option B — Your backend (production)

Mobile app calls your API (example path — you choose the URL):

http
POST https://api.yourapp.com/calling/token
Authorization: Bearer <sessionToken>
  • sessionToken = whatever your app already has after login.
  • Your server validates that session, then calls Alaznah:
http
POST https://www.alaznah.com/api/v1/calling/token
Authorization: Bearer ac_live_YOUR_API_KEY
Content-Type: application/json

{ "userId": "alice", "ttlSeconds": 900 }

Your API returns { "token": "<calling-jwt>" } to the app. Never put ac_live_… in the client.

Step-by-step integration + pseudo-code: Authentication.

userId rules

RuleWhy
Stable per end userPeers dial this id
Must match the token claimMismatch → auth / connect failures
Different on each test deviceSame JWT / same userId on both phones → the other side never rings correctly

4. Minimal app — provider + call UI

Pick one:

PathMountBest for
A — CallingScreenBuilt-in dialer + incoming + activeFirst call / demos
B — CallingUIYour screens + Alaznah overlayReal chat / contacts apps

CallingUI is exported from @alaznah/calling. Advanced pieces (IncomingCallScreen, theme helpers, etc.) stay on @alaznah/calling/ui. More detail: Call UI.

Path A — CallingScreen

tsx
import React from 'react';
import {
  CallingProvider,
  CallingScreen,
  DEFAULT_HOSTED_SIGNALING_URL,
} from '@alaznah/calling';

// Device A: 'alice' / Device B: 'bob' — must match the token you minted
const APP_USER_ID = 'alice';
const DEFAULT_PEER_ID = 'bob';

async function fetchCallingToken(): Promise<string> {
  // Demo: return a Console Test Token minted for APP_USER_ID
  // return 'eyJ...';

  // Production: sessionToken comes from YOUR app login — see Authentication docs
  const res = await fetch('https://api.yourapp.com/calling/token', {
    headers: { Authorization: `Bearer ${sessionToken}` },
  });
  if (!res.ok) throw new Error('Failed to mint calling token');
  const { token } = await res.json();
  return token;
}

export default function App() {
  return (
    <CallingProvider
      config={{
        userId: APP_USER_ID,
        getAuthToken: fetchCallingToken,
        // Hosted Signaling is the default — omit signalingUrl, or set explicitly:
        signalingUrl: DEFAULT_HOSTED_SIGNALING_URL, // wss://signal.alaznah.com
      }}
    >
      <CallingScreen
        userId={APP_USER_ID}
        defaultPeerId={DEFAULT_PEER_ID}
        onError={(e) => console.warn('calling', e)}
      />
    </CallingProvider>
  );
}

Wrap the navigator in CallingProvider, mount CallingUI once, then call client.startCall(...) from chat/contacts.

tsx
import React from 'react';
import { Button } from 'react-native';
import {
  CallingProvider,
  CallingUI,
  useCallingClient,
  useCallingReady,
  requestCallPermissions,
  DEFAULT_HOSTED_SIGNALING_URL,
} from '@alaznah/calling';
const APP_USER_ID = 'alice';

async function fetchCallingToken(): Promise<string> {
  // sessionToken = YOUR app login token — see Authentication docs
  const res = await fetch('https://api.yourapp.com/calling/token', {
    headers: { Authorization: `Bearer ${sessionToken}` },
  });
  if (!res.ok) throw new Error('Failed to mint calling token');
  const { token } = await res.json();
  return token;
}

function CallingOverlay() {
  const client = useCallingClient();
  return (
    <CallingUI
      client={client}
      onError={(e) => console.warn('calling', e)}
    />
  );
}

function ChatHeader({ peerId }: { peerId: string }) {
  const client = useCallingClient();
  const ready = useCallingReady();

  const call = async (mediaType: 'audio' | 'video') => {
    if (!ready) return;
    await requestCallPermissions(mediaType);
    await client.startCall({ calleeId: peerId, mediaType });
  };

  return (
    <>
      <Button title="Audio" disabled={!ready} onPress={() => void call('audio')} />
      <Button title="Video" disabled={!ready} onPress={() => void call('video')} />
    </>
  );
}

export default function App() {
  return (
    <CallingProvider
      config={{
        userId: APP_USER_ID,
        getAuthToken: fetchCallingToken,
        signalingUrl: DEFAULT_HOSTED_SIGNALING_URL,
      }}
    >
      {/* Your navigator / chat screens */}
      <ChatHeader peerId="bob" />
      <CallingOverlay />
    </CallingProvider>
  );
}

Without CallingUI (or CallingScreen), startCall may connect with no visible UI.

Signaling

SetupConfig
Hosted (recommended)Omit signalingUrl, or set DEFAULT_HOSTED_SIGNALING_URL (wss://signal.alaznah.com)
Self-hostedSet signalingUrl to your WSS endpoint — Self-hosting

Do not point a physical device at your laptop’s localhost without a tunnel.

Permissions before dialing

API is a string, not { video: boolean }:

ts
import { requestCallPermissions } from '@alaznah/calling';

await requestCallPermissions('audio'); // voice call
await requestCallPermissions('video'); // video call (mic + camera)

Call the matching mode before client.startCall(...) (Path B). CallingScreen (Path A) should do the same in its dial actions.


5. Place a call

On device A (userId: alice)

  1. Wait until the client is connected (valid token + signaling).
  2. Dial peer id bob (Path A dialer, or Path B audio/video buttons).
  3. Choose audio or video.

On device B (userId: bob)

  1. Same project / hosted signaling.
  2. Token minted for bob (not alice’s token).
  3. Incoming screen appears.

Accept / decline

ActionHow
AcceptSwipe up on the green control — tap does not accept
DeclineRed decline control

Foreground calling works once both clients are online with valid tokens. Background / kill-state ringing is a later step (below).


6. Verify checklist

Use this before filing a bug:

  • @alaznah/calling and required peers installed (react-native-webrtc, react-native-incall-manager, @react-native-community/netinfo, react-native-svg)
  • pod install (iOS) and a native rebuild after install / plist / manifest
  • Mic (and camera for video) usage strings / manifest permissions present
  • Device A and B have different userIds matching their tokens
  • Tokens not expired; getAuthToken returns a JWT for that userId
  • Both devices can reach Hosted Signaling (or your signalingUrl)
  • Accept uses swipe up, not tap
  • Testing on a physical device when media fails on simulator

7. Common failures

SymptomFix
WebRTC adapters not foundInstall peers — step 1 · Installation
must be used within CallingProviderWrap the tree that uses calling hooks/UI
Auth / 401 / reconnect exhaustedFresh token; userId matches JWT; Authentication
startCall connects but no UIYou must mount CallingScreen or CallingUICall UI
Accept “does nothing”Swipe up — Incoming Calls
Android AAR / compileSdk 36 forcedCompatibility
No ring when app is killedPush not configured yet — optional step below

More: Troubleshooting.


8. (Optional) Background ringing

Skip for the first successful foreground call. When you need ring-in-background / kill-state:


Next

GoalGuide
Chat app + overlay UICall UI — Path B (CallingUI)
Tokens in depthAuthentication
Voice-only / video guidesVoice Calls · Video Calls
Themes / custom screensCustomization · UI Components
VersionsCompatibility