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

@vaiftech/sdk-expo

v1.0.9

Published

VAIF SDK for React Native and Expo applications

Downloads

777

Readme

@vaiftech/sdk-expo

React Native and Expo SDK for VAIF Studio - a Backend-as-a-Service platform.

Installation

npm install @vaiftech/sdk-expo @vaiftech/client @react-native-async-storage/async-storage
# or
pnpm add @vaiftech/sdk-expo @vaiftech/client @react-native-async-storage/async-storage
# or
yarn add @vaiftech/sdk-expo @vaiftech/client @react-native-async-storage/async-storage

Quick Start

import { VaifProvider } from '@vaiftech/sdk-expo';
import { createVaifClient } from '@vaiftech/client';
import AsyncStorage from '@react-native-async-storage/async-storage';

const client = createVaifClient({
  baseUrl: 'https://api.myproject.vaif.io',
  apiKey: 'vaif_pk_xxx',
});

export default function App() {
  return (
    <VaifProvider client={client} storage={AsyncStorage}>
      <MainApp />
    </VaifProvider>
  );
}

Hooks Overview

| Category | Hooks | |----------|-------| | Auth | useAuth, useUser, useToken, usePasswordReset, useEmailVerification, useMagicLink, useOAuth, useMFA | | Query | useQuery, useQueryById, useQueryFirst, usePaginatedQuery, useInfiniteQuery, useCount | | Mutation | useCreate, useUpdate, useDelete, useUpsert, useBatchCreate, useBatchUpdate, useBatchDelete, useOptimisticMutation | | Realtime | useSubscription, useChannel, usePresence, useRealtimeConnection, useBroadcast | | Storage | useUpload, useDownload, useFile, useFiles, usePublicUrl | | Functions | useFunction, useRpc, useFunctionQuery, useFunctionList, useBatchInvoke, useScheduledFunction | | MongoDB | useMongoFind, useMongoFindOne, useMongoAggregate, useMongoInsertOne, useMongoInsertMany, useMongoUpdateOne, useMongoUpdateMany, useMongoDeleteOne, useMongoDeleteMany, useMongoInfiniteFind, useMongoCount, useMongoDistinct, useMongoCollection | | Observability | useMetrics, useAuditLogs, useIncidents, useSystemHealth, useRealtimeStats, useErrorTracking |

Features

Persistent Sessions

Sessions are automatically persisted using AsyncStorage, so users stay logged in:

import { useAuth } from '@vaiftech/sdk-expo';

function LoginScreen() {
  const { user, isLoading, signIn } = useAuth();

  // Session restored from AsyncStorage on app start
  if (isLoading) return <ActivityIndicator />;
  if (user) return <Redirect to="/home" />;

  return (
    <Button
      title="Sign In"
      onPress={() => signIn({ email, password })}
    />
  );
}

Image Upload from Camera/Gallery

Upload images directly from Expo ImagePicker:

import { useUpload } from '@vaiftech/sdk-expo';
import * as ImagePicker from 'expo-image-picker';

function ImageUpload() {
  const { upload, isUploading, progress } = useUpload();

  const pickAndUpload = async () => {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      quality: 0.8,
    });

    if (!result.canceled) {
      const { uri } = result.assets[0];
      // Convert URI to blob and upload
      const response = await fetch(uri);
      const blob = await response.blob();
      const uploaded = await upload(blob, `photos/${Date.now()}.jpg`);
      console.log('Uploaded:', uploaded?.url);
    }
  };

  return (
    <View>
      <Button
        title={isUploading ? `Uploading ${progress}%` : 'Select Image'}
        onPress={pickAndUpload}
        disabled={isUploading}
      />
    </View>
  );
}

Authentication

import { useAuth, useUser, useOAuth, useMFA } from '@vaiftech/sdk-expo';

function AuthScreen() {
  const { user, isLoading, signIn, signUp, signOut } = useAuth();

  if (isLoading) return <ActivityIndicator />;

  if (!user) {
    return (
      <View>
        <Button title="Sign In" onPress={() => signIn({ email, password })} />
        <Button title="Sign Up" onPress={() => signUp({ email, password, name })} />
      </View>
    );
  }

  return (
    <View>
      <Text>Welcome, {user.email}</Text>
      <Button title="Sign Out" onPress={signOut} />
    </View>
  );
}

// OAuth login
function OAuthLogin() {
  const { signInWithProvider, isLoading } = useOAuth();

  return (
    <View>
      <Button
        title="Sign in with Google"
        onPress={() => signInWithProvider('google')}
        disabled={isLoading}
      />
      <Button
        title="Sign in with Apple"
        onPress={() => signInWithProvider('apple')}
        disabled={isLoading}
      />
    </View>
  );
}

// MFA setup
function MFASetup() {
  const { enroll, verify, qrCode } = useMFA();

  return (
    <View>
      {qrCode && <Image source={{ uri: qrCode }} style={{ width: 200, height: 200 }} />}
      <Button title="Enable MFA" onPress={() => enroll({ type: 'totp' })} />
    </View>
  );
}

Standalone Auth Provider

For auth-only applications:

import { AuthProvider, useAuth, useSession } from '@vaiftech/sdk-expo';

function App() {
  return (
    <AuthProvider config={{ url: 'https://api.vaif.studio', apiKey: 'your-key' }}>
      <Navigation />
    </AuthProvider>
  );
}

Data Fetching

import { useQuery, useQueryById, usePaginatedQuery, useInfiniteQuery } from '@vaiftech/sdk-expo';

interface Task {
  id: string;
  title: string;
  completed: boolean;
}

// Basic query
function TaskList() {
  const { data: tasks, isLoading, error, refetch } = useQuery<Task>('tasks', {
    filters: [{ field: 'completed', operator: 'eq', value: false }],
    orderBy: [{ field: 'createdAt', direction: 'desc' }],
  });

  if (isLoading) return <ActivityIndicator />;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <FlatList
      data={tasks}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text>{item.title}</Text>}
      refreshing={isLoading}
      onRefresh={refetch}
    />
  );
}

// Query by ID
function TaskDetail({ taskId }: { taskId: string }) {
  const { data: task, isLoading } = useQueryById<Task>('tasks', taskId);

  if (isLoading) return <ActivityIndicator />;
  return <Text>{task?.title}</Text>;
}

// Paginated query
function PaginatedTasks() {
  const { data, page, totalPages, nextPage, prevPage } = usePaginatedQuery<Task>('tasks', {
    pageSize: 20,
  });

  return (
    <View>
      <FlatList data={data} renderItem={({ item }) => <Text>{item.title}</Text>} />
      <View style={{ flexDirection: 'row' }}>
        <Button title="Prev" onPress={prevPage} disabled={page === 1} />
        <Text>{page} / {totalPages}</Text>
        <Button title="Next" onPress={nextPage} disabled={page === totalPages} />
      </View>
    </View>
  );
}

// Infinite scroll
function InfiniteTasks() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery<Task>('tasks');

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text>{item.title}</Text>}
      onEndReached={() => hasNextPage && fetchNextPage()}
      ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
    />
  );
}

Mutations

import { useCreate, useUpdate, useDelete } from '@vaiftech/sdk-expo';

// Create
function CreateTask() {
  const { create, isCreating } = useCreate<Task>('tasks');

  return (
    <Button
      title={isCreating ? 'Creating...' : 'Add Task'}
      onPress={() => create({ title: 'New Task', completed: false })}
      disabled={isCreating}
    />
  );
}

// Update
function ToggleTask({ task }: { task: Task }) {
  const { update, isUpdating } = useUpdate<Task>('tasks');

  return (
    <Switch
      value={task.completed}
      onValueChange={(completed) => update(task.id, { completed })}
      disabled={isUpdating}
    />
  );
}

// Delete
function DeleteTask({ taskId }: { taskId: string }) {
  const { remove, isDeleting } = useDelete('tasks');

  return (
    <Button
      title="Delete"
      onPress={() => remove(taskId)}
      disabled={isDeleting}
      color="red"
    />
  );
}

Realtime

import { useSubscription, usePresence, useBroadcast, useChannel } from '@vaiftech/sdk-expo';

// Listen for new messages
function MessageListener() {
  useSubscription<Message>('messages', {
    filters: [{ field: 'room_id', operator: 'eq', value: roomId }],
    onInsert: (message) => console.log('New:', message),
    onUpdate: (message) => console.log('Updated:', message),
    onDelete: (message) => console.log('Deleted:', message.id),
  });

  return null;
}

// Track online users
function OnlineUsers({ roomId }: { roomId: string }) {
  const { users, track, leave } = usePresence(`room-${roomId}`);

  useEffect(() => {
    track({ status: 'online', name: currentUser.name });
    return () => leave();
  }, []);

  return (
    <View>
      <Text>Online: {users.length}</Text>
      {users.map(u => <Text key={u.id}>{u.name}</Text>)}
    </View>
  );
}

// Broadcast typing indicator
function TypingIndicator({ roomId }: { roomId: string }) {
  const { send, subscribe } = useBroadcast(`room-${roomId}`);
  const [typing, setTyping] = useState<string[]>([]);

  useEffect(() => {
    subscribe('typing', (event) => {
      setTyping(prev => [...prev, event.userName]);
      setTimeout(() => {
        setTyping(prev => prev.filter(n => n !== event.userName));
      }, 3000);
    });
  }, []);

  return (
    <Text>{typing.length > 0 ? `${typing.join(', ')} typing...` : ''}</Text>
  );
}

// Channel messaging
function ChatRoom({ roomId }: { roomId: string }) {
  const channel = useChannel(`room-${roomId}`);

  useEffect(() => {
    channel.on('message', (msg) => {
      // Handle incoming message
    });
    return () => channel.off('message');
  }, [channel]);

  const sendMessage = (text: string) => {
    channel.send('message', { text, userId: currentUser.id });
  };

  return <View>{/* Chat UI */}</View>;
}

Storage

import { useUpload, useDownload, useFile, useFiles, usePublicUrl } from '@vaiftech/sdk-expo';

// Upload with progress
function FileUpload() {
  const { upload, isUploading, progress } = useUpload();

  return (
    <View>
      <Button
        title={isUploading ? `${progress}%` : 'Upload'}
        onPress={async () => {
          const result = await upload(fileBlob, 'uploads/file.pdf');
          Alert.alert('Uploaded!', result?.url);
        }}
        disabled={isUploading}
      />
      {isUploading && <ProgressBar progress={progress / 100} />}
    </View>
  );
}

// Download file
function DownloadFile({ path }: { path: string }) {
  const { download, isDownloading } = useDownload();

  return (
    <Button
      title={isDownloading ? 'Downloading...' : 'Download'}
      onPress={async () => {
        const blob = await download(path);
        // Save to device or open
      }}
    />
  );
}

// Display image
function ProfileImage({ path }: { path: string }) {
  const url = usePublicUrl(path);
  return url ? <Image source={{ uri: url }} style={styles.avatar} /> : null;
}

// List files
function FileList({ folder }: { folder: string }) {
  const { files, isLoading } = useFiles({ prefix: folder });

  if (isLoading) return <ActivityIndicator />;

  return (
    <FlatList
      data={files}
      keyExtractor={(f) => f.path}
      renderItem={({ item }) => (
        <Text>{item.name} ({item.size} bytes)</Text>
      )}
    />
  );
}

Edge Functions

import { useFunction, useRpc, useBatchInvoke } from '@vaiftech/sdk-expo';

// Invoke function
function ProcessImage() {
  const { invoke, isInvoking, result, error } = useFunction('process-image');

  return (
    <Button
      title={isInvoking ? 'Processing...' : 'Process'}
      onPress={() => invoke({ imageUrl: 'https://...' })}
      disabled={isInvoking}
    />
  );
}

// RPC-style call
function Calculator() {
  const { call, isLoading, data } = useRpc<{ result: number }>('calculate');

  return (
    <View>
      <Button title="Calculate" onPress={() => call({ a: 5, b: 3, op: 'add' })} />
      {data && <Text>Result: {data.result}</Text>}
    </View>
  );
}

// Batch operations
function BatchProcessor() {
  const { invoke, isInvoking, results } = useBatchInvoke();

  const processAll = async () => {
    await invoke([
      { name: 'resize', payload: { url: 'img1.jpg' } },
      { name: 'resize', payload: { url: 'img2.jpg' } },
    ]);
  };

  return <Button title="Process All" onPress={processAll} disabled={isInvoking} />;
}

MongoDB Hooks

import {
  useMongoFind,
  useMongoFindOne,
  useMongoInsertOne,
  useMongoUpdateOne,
  useMongoDeleteOne,
  useMongoAggregate,
  useMongoInfiniteFind,
  useMongoCollection,
} from '@vaiftech/sdk-expo';

// Find documents
function UserList() {
  const { data: users, isLoading, refetch } = useMongoFind<User>('users', {
    filter: { status: 'active' },
    sort: { createdAt: -1 },
    limit: 20,
  });

  if (isLoading) return <ActivityIndicator />;

  return (
    <FlatList
      data={users}
      keyExtractor={(item) => item._id}
      renderItem={({ item }) => <Text>{item.name}</Text>}
      refreshing={isLoading}
      onRefresh={refetch}
    />
  );
}

// Find single document
function UserProfile({ id }: { id: string }) {
  const { data: user, isLoading } = useMongoFindOne<User>('users', { _id: id });
  return user ? <Text>{user.name}</Text> : null;
}

// Insert document
function CreateUser() {
  const { insertOne, isInserting } = useMongoInsertOne<User>('users');

  return (
    <Button
      title={isInserting ? 'Creating...' : 'Create User'}
      onPress={() => insertOne({ name: 'New User', email: '[email protected]' })}
      disabled={isInserting}
    />
  );
}

// Update document
function UpdateUser({ id }: { id: string }) {
  const { updateOne, isUpdating } = useMongoUpdateOne('users');

  return (
    <Button
      title="Update"
      onPress={() => updateOne({ _id: id }, { $set: { updatedAt: new Date() } })}
      disabled={isUpdating}
    />
  );
}

// Delete document
function DeleteUser({ id }: { id: string }) {
  const { deleteOne, isDeleting } = useMongoDeleteOne('users');

  return (
    <Button
      title="Delete"
      onPress={() => deleteOne({ _id: id })}
      disabled={isDeleting}
      color="red"
    />
  );
}

// Aggregation
function UserStats() {
  const { data: stats } = useMongoAggregate<{ _id: string; count: number }>('users', [
    { $match: { status: 'active' } },
    { $group: { _id: '$country', count: { $sum: 1 } } },
    { $sort: { count: -1 } },
  ]);

  return (
    <View>
      {stats?.map(stat => (
        <Text key={stat._id}>{stat._id}: {stat.count}</Text>
      ))}
    </View>
  );
}

// Infinite scroll
function InfiniteUsers() {
  const { data, fetchNextPage, hasNextPage } = useMongoInfiniteFind<User>('users', {
    filter: { status: 'active' },
    limit: 20,
  });

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item._id}
      renderItem={({ item }) => <Text>{item.name}</Text>}
      onEndReached={() => hasNextPage && fetchNextPage()}
    />
  );
}

// Full collection access
function BulkOps() {
  const collection = useMongoCollection<User>('users');

  const handleBulk = async () => {
    const count = await collection.count({ status: 'active' });
    const countries = await collection.distinct('country');
    await collection.bulkWrite([
      { insertOne: { document: { name: 'Bulk User' } } },
    ]);
  };

  return <Button title="Run Bulk Ops" onPress={handleBulk} />;
}

Observability Hooks

import {
  useMetrics,
  useAuditLogs,
  useIncidents,
  useSystemHealth,
  useRealtimeStats,
  useErrorTracking,
} from '@vaiftech/sdk-expo';

// Metrics dashboard
function MetricsDashboard() {
  const { metrics, timeRange, setTimeRange, refresh } = useMetrics({
    sources: ['api', 'database'],
    interval: '1h',
  });

  return (
    <View>
      <Picker selectedValue={timeRange} onValueChange={setTimeRange}>
        <Picker.Item label="Last Hour" value="1h" />
        <Picker.Item label="Last 24 Hours" value="24h" />
        <Picker.Item label="Last 7 Days" value="7d" />
      </Picker>
      {metrics?.map(m => (
        <Text key={m.name}>{m.name}: {m.value}</Text>
      ))}
    </View>
  );
}

// Audit logs
function AuditLogs() {
  const { logs, hasMore, loadMore } = useAuditLogs({ limit: 50 });

  return (
    <FlatList
      data={logs}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Text>{item.timestamp}: {item.action} by {item.userId}</Text>
      )}
      onEndReached={() => hasMore && loadMore()}
    />
  );
}

// Incidents
function IncidentList() {
  const { incidents, activeCount, acknowledge, resolve } = useIncidents();

  return (
    <View>
      <Text>Active: {activeCount}</Text>
      {incidents?.map(incident => (
        <View key={incident.id}>
          <Text>{incident.title} - {incident.severity}</Text>
          {incident.status === 'open' && (
            <Button title="Ack" onPress={() => acknowledge(incident.id)} />
          )}
          {incident.status === 'acknowledged' && (
            <Button title="Resolve" onPress={() => resolve(incident.id)} />
          )}
        </View>
      ))}
    </View>
  );
}

// System health
function SystemHealth() {
  const { isHealthy, services, lastCheck } = useSystemHealth({ pollInterval: 30000 });

  return (
    <View>
      <Text>Status: {isHealthy ? 'Healthy' : 'Degraded'}</Text>
      <Text>Last: {lastCheck?.toLocaleString()}</Text>
      {services?.map(svc => (
        <Text key={svc.name}>{svc.name}: {svc.status} ({svc.latency}ms)</Text>
      ))}
    </View>
  );
}

// Realtime stats
function RealtimeStats() {
  const { connections, requestsPerSecond, activeUsers } = useRealtimeStats();

  return (
    <View>
      <Text>Connections: {connections}</Text>
      <Text>Requests/sec: {requestsPerSecond}</Text>
      <Text>Active Users: {activeUsers}</Text>
    </View>
  );
}

// Error tracking
function ErrorTracker() {
  const { errorRate, topErrors, trackError } = useErrorTracking({ timeRange: '24h' });

  return (
    <View>
      <Text>Error Rate: {errorRate}%</Text>
      {topErrors?.map(err => (
        <Text key={err.message}>{err.message} ({err.count})</Text>
      ))}
    </View>
  );
}

TypeScript Support

All hooks support TypeScript generics:

interface User {
  id: string;
  email: string;
  name: string;
}

const { data } = useQuery<User>('users');
// data is User[] | undefined

const { data: user } = useMongoFindOne<User>('users', { _id: id });
// user is User | null | undefined

Related Packages

License

MIT