npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@smarthivelabs-devs/hive-socket-expo

v1.1.0

Published

Expo/React Native SDK for Hive-Socket — real-time hooks and provider for mobile apps

Readme

@smarthivelabs-devs/hive-socket-expo

Expo / React Native SDK for Hive-Socket — real-time hooks and provider for mobile apps.

Same API as @smarthivelabs-devs/hive-socket-react. The only difference is transport: React Native does not support HTTP polling, so this package uses WebSocket-only, which works natively in Expo and bare React Native.

Install

npx expo install @smarthivelabs-devs/hive-socket-expo socket.io-client

For bare React Native:

npm install @smarthivelabs-devs/hive-socket-expo socket.io-client

Peer dependencies: react >= 18, react-native >= 0.73, socket.io-client ^4


Setup

1. Wrap your root layout with the provider

// app/_layout.tsx (Expo Router)
import { HiveSocketProvider } from '@smarthivelabs-devs/hive-socket-expo';
import { useAuth } from '@smarthivelabs-devs/auth-expo'; // your SmartHive Auth hook
import { Slot } from 'expo-router';

export default function RootLayout() {
  const { accessToken } = useAuth();

  return (
    <HiveSocketProvider
      url="https://socket.smarthivelabs.dev"
      token={accessToken ?? ''}
    >
      <Slot />
    </HiveSocketProvider>
  );
}

The provider reconnects automatically when the token changes (e.g. after a token refresh).

Without Expo Router:

// App.tsx
import { HiveSocketProvider } from '@smarthivelabs-devs/hive-socket-expo';

export default function App() {
  const { accessToken } = useAuth();

  return (
    <HiveSocketProvider url="https://socket.smarthivelabs.dev" token={accessToken ?? ''}>
      <NavigationContainer>
        {/* your app */}
      </NavigationContainer>
    </HiveSocketProvider>
  );
}

2. Use hooks in any screen or component

import {
  useHiveSocket,
  useMessages,
  usePresence,
  useNotifications,
} from '@smarthivelabs-devs/hive-socket-expo';

Hooks

useHiveSocket

Connection status and all send actions.

const {
  status,               // 'connecting' | 'connected' | 'disconnected'
  socketId,             // string | null

  joinRoom,             // (roomId: string) => void
  leaveRoom,            // (roomId: string) => void
  sendMessage,          // (roomId: string, type: string, payload: Record<string, unknown>) => void
  updatePresence,       // (status: 'online' | 'away' | 'offline') => void
  fetchHistory,         // (roomId: string, limit?: number, before?: string) => void
  markNotificationRead, // (notificationId: string) => void
} = useHiveSocket();

Connection status bar:

import { useHiveSocket } from '@smarthivelabs-devs/hive-socket-expo';
import { View, Text } from 'react-native';

function StatusBar() {
  const { status } = useHiveSocket();

  if (status === 'connected') return null;

  return (
    <View style={{ backgroundColor: '#f59e0b', padding: 8, alignItems: 'center' }}>
      <Text style={{ color: 'white', fontSize: 12 }}>
        {status === 'connecting' ? 'Connecting...' : 'Offline — reconnecting'}
      </Text>
    </View>
  );
}

useMessages(roomId)

Subscribe to real-time messages in a room. Automatically joins on mount, loads the last 50 messages, and leaves on unmount.

const messages = useMessages(roomId); // HiveMessage[]
import { useMessages, useHiveSocket } from '@smarthivelabs-devs/hive-socket-expo';
import { FlatList, TextInput, TouchableOpacity, View, Text, StyleSheet } from 'react-native';
import { useState } from 'react';

export default function ChatScreen({ roomId }: { roomId: string }) {
  const { status, sendMessage } = useHiveSocket();
  const messages = useMessages(roomId);
  const [text, setText] = useState('');

  const send = () => {
    if (!text.trim() || status !== 'connected') return;
    sendMessage(roomId, 'chat.message', { text });
    setText('');
  };

  return (
    <View style={styles.container}>
      <FlatList
        data={[...messages].reverse()}
        keyExtractor={(m) => m._eid}
        inverted
        renderItem={({ item }) => (
          <View style={styles.message}>
            <Text style={styles.sender}>{item.senderId}</Text>
            <Text>{item.payload.text as string}</Text>
          </View>
        )}
      />
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          value={text}
          onChangeText={setText}
          placeholder="Type a message..."
          returnKeyType="send"
          onSubmitEditing={send}
        />
        <TouchableOpacity
          style={[styles.sendBtn, status !== 'connected' && styles.sendBtnDisabled]}
          onPress={send}
          disabled={status !== 'connected'}
        >
          <Text style={{ color: 'white' }}>Send</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
  message: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#f0f0f0' },
  sender: { fontWeight: 'bold', marginBottom: 4 },
  inputRow: { flexDirection: 'row', padding: 12, borderTopWidth: 1, borderTopColor: '#eee' },
  input: { flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, paddingHorizontal: 12, marginRight: 8, height: 40 },
  sendBtn: { backgroundColor: '#2563eb', borderRadius: 8, paddingHorizontal: 16, justifyContent: 'center' },
  sendBtnDisabled: { backgroundColor: '#93c5fd' },
});

HiveMessage shape:

interface HiveMessage {
  type: string;                      // e.g. 'chat.message', 'quiz.answer'
  senderId: string;                  // userId of the sender
  roomId: string;
  payload: Record<string, unknown>;  // your custom data
  timestamp: number;                 // Unix ms
  _eid: string;                      // unique event ID — use as FlatList key
}

usePresence(userId)

Track another user's live presence status.

const presence = usePresence(userId);
// { status: 'online' | 'away' | 'offline', lastSeen: string } | null
import { usePresence } from '@smarthivelabs-devs/hive-socket-expo';
import { View, Image } from 'react-native';

export function UserAvatar({ userId, avatarUri }: { userId: string; avatarUri: string }) {
  const presence = usePresence(userId);

  return (
    <View style={{ position: 'relative' }}>
      <Image source={{ uri: avatarUri }} style={{ width: 40, height: 40, borderRadius: 20 }} />
      <View
        style={{
          position: 'absolute',
          bottom: 0,
          right: 0,
          width: 12,
          height: 12,
          borderRadius: 6,
          backgroundColor: presence?.status === 'online' ? '#22c55e' : '#9ca3af',
          borderWidth: 2,
          borderColor: 'white',
        }}
      />
    </View>
  );
}

Sync presence with app state (background / foreground):

import { useHiveSocket } from '@smarthivelabs-devs/hive-socket-expo';
import { AppState, AppStateStatus } from 'react-native';
import { useEffect } from 'react';

export function usePresenceSync() {
  const { updatePresence } = useHiveSocket();

  useEffect(() => {
    const sub = AppState.addEventListener('change', (state: AppStateStatus) => {
      updatePresence(state === 'active' ? 'online' : 'away');
    });
    return () => sub.remove();
  }, [updatePresence]);
}

Call usePresenceSync() once in your root layout to automatically mark the user as away when they background the app.


useNotifications

Receive real-time notifications pushed from your backends. Pending (unread) notifications load automatically on connect.

const {
  notifications,  // HiveNotification[]
  unreadCount,    // number
  markRead,       // (notificationId: string) => void
} = useNotifications();

Notification badge on tab bar:

import { useNotifications } from '@smarthivelabs-devs/hive-socket-expo';
import { View, Text } from 'react-native';

export function NotificationBadge() {
  const { unreadCount } = useNotifications();

  if (unreadCount === 0) return null;

  return (
    <View
      style={{
        position: 'absolute',
        top: -4,
        right: -4,
        backgroundColor: '#ef4444',
        borderRadius: 10,
        minWidth: 18,
        height: 18,
        paddingHorizontal: 4,
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <Text style={{ color: 'white', fontSize: 10, fontWeight: 'bold' }}>
        {unreadCount > 99 ? '99+' : unreadCount}
      </Text>
    </View>
  );
}

Notification list screen:

import { useNotifications } from '@smarthivelabs-devs/hive-socket-expo';
import { FlatList, View, Text, TouchableOpacity, StyleSheet } from 'react-native';

export default function NotificationsScreen() {
  const { notifications, markRead } = useNotifications();

  return (
    <FlatList
      data={notifications}
      keyExtractor={(n) => n.id}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={[styles.item, !item.read && styles.unread]}
          onPress={() => !item.read && markRead(item.id)}
        >
          <Text style={styles.title}>{item.title}</Text>
          <Text style={styles.body}>{item.body}</Text>
          <Text style={styles.time}>
            {new Date(item.createdAt).toLocaleString()}
          </Text>
        </TouchableOpacity>
      )}
      ListEmptyComponent={<Text style={{ textAlign: 'center', marginTop: 40, color: '#9ca3af' }}>No notifications</Text>}
    />
  );
}

const styles = StyleSheet.create({
  item: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#f0f0f0', backgroundColor: '#fff' },
  unread: { backgroundColor: '#eff6ff' },
  title: { fontWeight: 'bold', fontSize: 15, marginBottom: 4 },
  body: { color: '#374151', marginBottom: 4 },
  time: { color: '#9ca3af', fontSize: 12 },
});

HiveNotification shape:

interface HiveNotification {
  id: string;
  title: string;
  body: string;
  type: 'info' | 'success' | 'warning' | 'error' | 'message';
  metadata: Record<string, unknown>;
  sourceService?: string;   // which backend sent it
  read: boolean;
  readAt: string | null;    // ISO date string
  createdAt: string;        // ISO date string
}

useWebRTC

Peer-to-peer audio, video, screen sharing, and data channels over WebRTC. The existing Socket.IO connection handles signaling — media streams flow directly between peers. Requires react-native-webrtc for actual media on React Native (see note below).

const {
  callState,        // 'idle' | 'calling' | 'incoming' | 'connecting' | 'active' | 'ended'
  currentCall,      // HiveWebRtcCall | null

  localStream,      // MediaStream | null
  remoteStream,     // MediaStream | null
  screenStream,     // MediaStream | null
  dataChannel,      // RTCDataChannel | null

  isAudioMuted,     // boolean
  isVideoOff,       // boolean
  isSharingScreen,  // boolean

  startCall,        // (targetUserId: string, callType: WebRtcCallType) => Promise<void>
  acceptCall,       // () => Promise<void>
  rejectCall,       // () => void
  endCall,          // () => void

  toggleAudio,      // () => void
  toggleVideo,      // () => void
  startScreenShare, // () => Promise<void>
  stopScreenShare,  // () => void
  sendData,         // (data: string | ArrayBuffer) => void
} = useWebRTC();

WebRtcCallType: 'audio' | 'video' | 'screen' | 'data'

React Native / Expo setup:

React Native does not have a built-in RTCPeerConnection. Install and register react-native-webrtc first:

npx expo install react-native-webrtc

Then register the globals before any WebRTC code runs (e.g. in your root _layout.tsx):

import { registerGlobals } from 'react-native-webrtc';
registerGlobals(); // makes RTCPeerConnection, MediaStream, etc. available globally

Video call example (React Native):

import { useWebRTC } from '@smarthivelabs-devs/hive-socket-expo';
import { RTCView } from 'react-native-webrtc';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';

function VideoCallScreen({ targetUserId }: { targetUserId: string }) {
  const {
    callState,
    localStream,
    remoteStream,
    startCall,
    acceptCall,
    rejectCall,
    endCall,
    toggleAudio,
    toggleVideo,
    isAudioMuted,
    isVideoOff,
  } = useWebRTC();

  if (callState === 'incoming') {
    return (
      <View style={styles.center}>
        <Text style={styles.label}>Incoming call...</Text>
        <TouchableOpacity style={styles.acceptBtn} onPress={acceptCall}>
          <Text style={{ color: 'white' }}>Accept</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.rejectBtn} onPress={rejectCall}>
          <Text style={{ color: 'white' }}>Decline</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      {remoteStream && (
        <RTCView streamURL={remoteStream.toURL()} style={styles.remoteVideo} objectFit="cover" />
      )}
      {localStream && (
        <RTCView streamURL={localStream.toURL()} style={styles.localVideo} objectFit="cover" mirror />
      )}

      {callState === 'idle' ? (
        <TouchableOpacity style={styles.callBtn} onPress={() => startCall(targetUserId, 'video')}>
          <Text style={{ color: 'white' }}>Start Video Call</Text>
        </TouchableOpacity>
      ) : (
        <View style={styles.controls}>
          <TouchableOpacity style={styles.controlBtn} onPress={toggleAudio}>
            <Text style={{ color: 'white' }}>{isAudioMuted ? 'Unmute' : 'Mute'}</Text>
          </TouchableOpacity>
          <TouchableOpacity style={[styles.controlBtn, styles.endBtn]} onPress={endCall}>
            <Text style={{ color: 'white' }}>End</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.controlBtn} onPress={toggleVideo}>
            <Text style={{ color: 'white' }}>{isVideoOff ? 'Camera On' : 'Camera Off'}</Text>
          </TouchableOpacity>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#000' },
  center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  remoteVideo: { flex: 1 },
  localVideo: { position: 'absolute', top: 16, right: 16, width: 100, height: 140, borderRadius: 8 },
  controls: { position: 'absolute', bottom: 40, flexDirection: 'row', alignSelf: 'center', gap: 16 },
  controlBtn: { backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: 32, paddingVertical: 12, paddingHorizontal: 20 },
  endBtn: { backgroundColor: '#ef4444' },
  callBtn: { position: 'absolute', bottom: 40, alignSelf: 'center', backgroundColor: '#22c55e', borderRadius: 32, paddingVertical: 14, paddingHorizontal: 32 },
  label: { fontSize: 18, marginBottom: 24 },
  acceptBtn: { backgroundColor: '#22c55e', borderRadius: 32, paddingVertical: 14, paddingHorizontal: 32, marginBottom: 12 },
  rejectBtn: { backgroundColor: '#ef4444', borderRadius: 32, paddingVertical: 14, paddingHorizontal: 32 },
});

Audio-only call:

await startCall(targetUserId, 'audio');

Data channel:

const { startCall, dataChannel, sendData } = useWebRTC();

await startCall(targetUserId, 'data');

useEffect(() => {
  if (!dataChannel) return;
  dataChannel.onopen = () => sendData('hello peer');
  dataChannel.onmessage = (e) => console.log('received:', e.data);
}, [dataChannel]);

ICE / STUN configuration:

The server emits ICE server config automatically on connect (webrtc:config). The hook picks it up — no manual STUN/TURN configuration needed. To add a TURN server, set WEBRTC_TURN_URLS, WEBRTC_TURN_USERNAME, and WEBRTC_TURN_CREDENTIAL on the Hive-Socket server.


App background and reconnect

The provider automatically handles reconnect and missed event replay. When the app returns to the foreground and the socket reconnects, it sends reconnect:sync to the server which replays any events missed in the last 5 minutes. No extra code needed.

// Optional: listen for reconnect manually
const { status } = useHiveSocket();

useEffect(() => {
  if (status === 'connected') {
    // Already re-joined rooms and replayed missed events automatically
    console.log('Back online');
  }
}, [status]);

Complete example — Expo Router chat screen

// app/(tabs)/chat/[roomId].tsx
import { useMessages, useHiveSocket } from '@smarthivelabs-devs/hive-socket-expo';
import { useLocalSearchParams } from 'expo-router';
import { FlatList, KeyboardAvoidingView, Platform, TextInput, TouchableOpacity, View, Text } from 'react-native';
import { useState } from 'react';

export default function ChatScreen() {
  const { roomId } = useLocalSearchParams<{ roomId: string }>();
  const { status, sendMessage } = useHiveSocket();
  const messages = useMessages(roomId);
  const [text, setText] = useState('');

  const send = () => {
    if (!text.trim() || status !== 'connected') return;
    sendMessage(roomId, 'chat.message', { text });
    setText('');
  };

  return (
    <KeyboardAvoidingView
      style={{ flex: 1 }}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <FlatList
        data={[...messages].reverse()}
        keyExtractor={(m) => m._eid}
        inverted
        contentContainerStyle={{ padding: 16 }}
        renderItem={({ item }) => (
          <View style={{ marginBottom: 8 }}>
            <Text style={{ fontWeight: 'bold', marginBottom: 2 }}>{item.senderId}</Text>
            <Text>{item.payload.text as string}</Text>
          </View>
        )}
      />
      <View style={{ flexDirection: 'row', padding: 12, borderTopWidth: 1, borderTopColor: '#eee' }}>
        <TextInput
          style={{ flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 20, paddingHorizontal: 16, paddingVertical: 8, marginRight: 8 }}
          value={text}
          onChangeText={setText}
          placeholder="Message..."
          multiline
        />
        <TouchableOpacity
          onPress={send}
          disabled={status !== 'connected'}
          style={{ backgroundColor: '#2563eb', borderRadius: 20, paddingHorizontal: 20, justifyContent: 'center', opacity: status !== 'connected' ? 0.5 : 1 }}
        >
          <Text style={{ color: 'white', fontWeight: '600' }}>Send</Text>
        </TouchableOpacity>
      </View>
    </KeyboardAvoidingView>
  );
}

Room naming

| Room pattern | Use case | |-------------|----------| | project:{id}:general | Project-wide chat | | project:{id}:thread:{threadId} | Specific discussion thread | | course:{courseId} | Course-level broadcast | | event:{eventId} | Live voting session |

Each user is automatically in user:{userId} on connect — backend notifications always arrive without manual joinRoom.


Types

import type {
  HiveMessage,
  HiveHistoryMessage,
  HiveNotification,
  HivePresenceChange,
  HiveSocketError,
  HiveSocketProviderProps,
  NotificationType,
  PresenceStatus,
  ConnectionStatus,
  UseHiveSocketReturn,
  PresenceState,
  UseNotificationsReturn,
  // WebRTC
  WebRtcCallType,
  WebRtcCallState,
  HiveWebRtcCall,
  HiveIceServer,
  HiveWebRtcConfig,
  UseWebRTCReturn,
} from '@smarthivelabs-devs/hive-socket-expo';

| Type | Description | |------|-------------| | HiveMessage | Real-time message from a room | | HiveHistoryMessage | Message row from history fetch | | HiveNotification | User notification (real-time or pending) | | HivePresenceChange | Presence status change event | | HiveSocketError | Server error payload | | ConnectionStatus | 'connecting' \| 'connected' \| 'disconnected' | | PresenceStatus | 'online' \| 'away' \| 'offline' | | NotificationType | 'info' \| 'success' \| 'warning' \| 'error' \| 'message' | | WebRtcCallType | 'audio' \| 'video' \| 'screen' \| 'data' | | WebRtcCallState | 'idle' \| 'calling' \| 'incoming' \| 'connecting' \| 'active' \| 'ended' | | HiveWebRtcCall | Active call metadata (callId, callerId, targetId, callType) | | HiveIceServer | ICE server config (urls, optional username/credential) | | HiveWebRtcConfig | ICE config emitted by server on connect |


Security

How the token is sent and validated

When you pass token to HiveSocketProvider, the SDK sends it in the Socket.IO handshake auth object:

io(url, { auth: { token }, transports: ['websocket'] })

The server validates the JWT during the WebSocket handshake — before any events are processed. If the token is invalid or expired the connection is rejected with UNAUTHORIZED immediately.

After verification the server extracts userId and projectId directly from the JWT claims. You never send these yourself — they cannot be spoofed by the client.

What the server enforces

| Guarantee | How | |-----------|-----| | Token is valid and unexpired | JWT signature verified on every new connection | | User only receives their own notifications | Server auto-joins {projectId}::user:{userId} from the token — not from client input | | User can only join rooms in their project | Room joins are namespaced to the token's projectId server-side | | Stale tokens are rejected on reconnect | Every reconnect (foreground resume) re-runs full handshake verification |

What you must do

  • Pass the access token, not the refresh token. Access tokens are short-lived; the server rejects expired ones and disconnects cleanly.
  • Reconnect after a token refresh. The provider reconnects automatically every time token changes — SmartHive Auth's silent refresh triggers this with no extra code.
  • Use token={accessToken ?? ''} at the root layout. The provider skips connecting when the token is falsy — safe to render before auth is ready.
  • Sync presence on foreground/background using AppState (see usePresenceSync above) so the server knows when the user is active.

Token lifecycle example (Expo Router)

// app/_layout.tsx
import { HiveSocketProvider } from '@smarthivelabs-devs/hive-socket-expo';
import { useAuth } from '@smarthivelabs-devs/auth-expo'; // SmartHive Auth hook
import { Slot } from 'expo-router';

export default function RootLayout() {
  const { accessToken } = useAuth(); // null before auth resolves, auto-refreshed

  // Provider skips connecting when token is falsy — safe to render unconditionally
  return (
    <HiveSocketProvider url="https://socket.smarthivelabs.dev" token={accessToken ?? ''}>
      <Slot />
    </HiveSocketProvider>
  );
}

When SmartHive Auth silently refreshes the access token, accessToken changes → provider tears down the old socket and reconnects with the new token automatically.


Troubleshooting

Connection fails — ERR_CONNECTION_REFUSED or immediate disconnect

  • Ensure the Hive-Socket server allows WebSocket connections on port 443
  • The Expo SDK uses transports: ['websocket'] only — if the server requires polling for the handshake, check that Nginx has the WebSocket upgrade headers configured

Token is empty string on mount If your auth hook returns null before the token is ready, pass ''. The provider skips connecting when the token is falsy and connects automatically once it becomes a non-empty string.

<HiveSocketProvider url={url} token={accessToken ?? ''}>

Messages not loading useMessages waits for status === 'connected' before joining and fetching. If the connection never reaches 'connected', check that the token is valid and the server URL is reachable from the device.

Metro bundler errors with socket.io-client Add a resolver alias if needed:

// metro.config.js
const config = {
  resolver: {
    extraNodeModules: {
      'socket.io-client': require.resolve('socket.io-client'),
    },
  },
};

Android: connection works on emulator but not on device If testing on a real device with a local server (http://192.168.x.x), ensure the device is on the same network and the port is open. In production, always use HTTPS.