Expo + Vettly
Add content moderation to your Expo / React Native app.
Installation
bash
npx expo install @nextauralabs/vettly-sdkQuick Start
tsx
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: 'vettly_your_api_key' // Use env var in production
})
async function checkMessage(content: string) {
const result = await vettly.check({
content,
contentType: 'text'
})
return result.action !== 'block'
}Text Moderation
Chat Input Component
tsx
import React, { useState } from 'react'
import { View, TextInput, Button, Text, StyleSheet } from 'react-native'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})
export function ChatInput({ onSend }) {
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const [checking, setChecking] = useState(false)
const handleSend = async () => {
if (!message.trim()) return
setChecking(true)
setError('')
try {
const result = await vettly.check({
content: message,
contentType: 'text'
})
if (result.action === 'block') {
setError('Message contains inappropriate content')
return
}
onSend(message)
setMessage('')
} catch (err) {
console.error('Moderation error:', err)
// Decide: block or allow on error
onSend(message) // Fail open
setMessage('')
} finally {
setChecking(false)
}
}
return (
<View style={styles.container}>
<TextInput
style={styles.input}
value={message}
onChangeText={setMessage}
placeholder="Type a message..."
multiline
/>
{error ? <Text style={styles.error}>{error}</Text> : null}
<Button
title={checking ? 'Checking...' : 'Send'}
onPress={handleSend}
disabled={checking || !message.trim()}
/>
</View>
)
}
const styles = StyleSheet.create({
container: { padding: 16 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
marginBottom: 8
},
error: { color: 'red', marginBottom: 8 }
})Image Moderation
With expo-image-picker
tsx
import * as ImagePicker from 'expo-image-picker'
import * as FileSystem from 'expo-file-system'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})
async function pickAndModerateImage() {
// 1. Pick image
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8
})
if (result.canceled) return null
const imageUri = result.assets[0].uri
// 2. Convert to base64
const base64 = await FileSystem.readAsStringAsync(imageUri, {
encoding: FileSystem.EncodingType.Base64
})
// 3. Moderate
const modResult = await vettly.check({
content: base64,
contentType: 'image'
})
if (modResult.action === 'block') {
Alert.alert('Error', 'This image is not allowed')
return null
}
return imageUri
}Image Upload Component
tsx
import React, { useState } from 'react'
import { View, Image, Button, Alert, ActivityIndicator } from 'react-native'
import * as ImagePicker from 'expo-image-picker'
import * as FileSystem from 'expo-file-system'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})
export function AvatarUpload({ onUpload }) {
const [image, setImage] = useState(null)
const [loading, setLoading] = useState(false)
const pickImage = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert('Permission needed', 'Please allow photo access')
return
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.8
})
if (result.canceled) return
setLoading(true)
try {
const uri = result.assets[0].uri
// Convert to base64
const base64 = await FileSystem.readAsStringAsync(uri, {
encoding: FileSystem.EncodingType.Base64
})
// Moderate
const modResult = await vettly.check({
content: base64,
contentType: 'image'
})
if (modResult.action === 'block') {
Alert.alert(
'Image Not Allowed',
'Please choose a different image'
)
return
}
setImage(uri)
onUpload(uri, modResult.decisionId)
} catch (error) {
console.error('Error:', error)
Alert.alert('Error', 'Failed to process image')
} finally {
setLoading(false)
}
}
return (
<View style={{ alignItems: 'center' }}>
{loading ? (
<ActivityIndicator size="large" />
) : image ? (
<Image
source={{ uri: image }}
style={{ width: 150, height: 150, borderRadius: 75 }}
/>
) : null}
<Button
title={image ? 'Change Photo' : 'Upload Photo'}
onPress={pickImage}
disabled={loading}
/>
</View>
)
}Custom Hook
tsx
// hooks/useModeration.ts
import { useState, useCallback } from 'react'
import { ModerationClient, ModerationResult } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})
export function useModeration() {
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<ModerationResult | null>(null)
const [error, setError] = useState<Error | null>(null)
const check = useCallback(async (
content: string,
contentType: 'text' | 'image' = 'text'
) => {
setLoading(true)
setError(null)
try {
const modResult = await vettly.check({ content, contentType })
setResult(modResult)
return modResult
} catch (err) {
setError(err as Error)
throw err
} finally {
setLoading(false)
}
}, [])
const isBlocked = result?.action === 'block'
const isFlagged = result?.action === 'flag'
return { check, result, loading, error, isBlocked, isFlagged }
}Usage:
tsx
function CommentForm() {
const [comment, setComment] = useState('')
const { check, loading, isBlocked } = useModeration()
const handleSubmit = async () => {
const result = await check(comment)
if (result.action !== 'block') {
// Submit comment
await submitComment(comment)
}
}
return (
<View>
<TextInput value={comment} onChangeText={setComment} />
{isBlocked && <Text style={{ color: 'red' }}>Not allowed</Text>}
<Button
title={loading ? 'Checking...' : 'Submit'}
onPress={handleSubmit}
disabled={loading}
/>
</View>
)
}Server-Side (Recommended for Production)
For production, moderate on your backend to keep API keys secure:
tsx
// Mobile app
async function sendMessage(content: string) {
const response = await fetch('https://your-api.com/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify({ content })
})
if (response.status === 403) {
const data = await response.json()
Alert.alert('Not Allowed', data.error)
return
}
return response.json()
}ts
// Your backend (Express/Next.js/etc)
app.post('/messages', async (req, res) => {
const { content } = req.body
const result = await vettly.check({ content, contentType: 'text' })
if (result.action === 'block') {
return res.status(403).json({ error: 'Content not allowed' })
}
// Save message
await db.messages.create({ content, userId: req.user.id })
res.json({ success: true })
})Environment Variables
bash
# .env
EXPO_PUBLIC_VETTLY_API_KEY=vettly_your_api_keyAccess in code:
ts
const apiKey = process.env.EXPO_PUBLIC_VETTLY_API_KEYOffline Handling
tsx
import NetInfo from '@react-native-community/netinfo'
async function moderateWithFallback(content: string) {
const netInfo = await NetInfo.fetch()
if (!netInfo.isConnected) {
// Queue for later moderation
await AsyncStorage.setItem(
'pendingModeration',
JSON.stringify([...pending, { content, timestamp: Date.now() }])
)
return { action: 'pending' }
}
return vettly.check({ content, contentType: 'text' })
}Next Steps
- React Components - Pre-built UI components
- Custom Policies - Configure moderation rules
- Best Practices - Mobile moderation patterns