Appendix A: Convex Functions
Keep your API keys safe by running sensitive code on the server.
Why Edge Functions?
Never expose API keys in your React Native app. Anyone can decompile your app and steal them.
Edge Functions run on Convex's servers, keeping your secrets safe:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ React Native │────▶│ Edge Function │────▶│ OpenAI API │
│ (Client) │ │ (Server-side) │ │ │
│ │ │ API key hidden │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘Setup
1. Install Convex CLI
# macOS
brew install convex/tap/convex
# npm (alternative)
npm install -g convex2. Initialize in Your Project
convex initThis creates a convex/ folder:
convex/
├── config.toml
└── functions/3. Create Your First Function
convex functions new generate-imageExample: AI Image Generation
Edge Function
convex/functions/generate-image/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@convex/convex-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// Get the authorization header
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
throw new Error('No authorization header');
}
// Create Convex client with user's token
const convexClient = createClient(
process.env.CONVEX_URL ?? '',
process.env.CONVEX_AUTH_TOKEN ?? '',
{ global: { headers: { Authorization: authHeader } } }
);
// Verify user is authenticated
const { data: { user }, error: userError } = await convexClient.auth.getUser();
if (userError || !user) {
throw new Error('Unauthorized');
}
// Get request body
const { prompt } = await req.json();
if (!prompt) {
throw new Error('Prompt is required');
}
// Call OpenAI API (key is server-side only!)
const openaiKey = Deno.env.get('OPENAI_API_KEY');
const response = await fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openaiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'dall-e-3',
prompt,
n: 1,
size: '1024x1024',
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error?.message || 'OpenAI API error');
}
// Save to database
const imageUrl = data.data[0].url;
await convexClient.from('generations').insert({
user_id: user.id,
prompt,
result_url: imageUrl,
type: 'image',
});
return new Response(
JSON.stringify({ imageUrl }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
});Client Code
services/ai.service.ts
import { convex } from '@/lib/convex';
export async function generateImage(prompt: string): Promise<string> {
const { data, error } = await convex.functions.invoke('generate-image', {
body: { prompt },
});
if (error) {
throw new Error(error.message);
}
return data.imageUrl;
}In your component
const handleGenerate = async () => {
try {
setLoading(true);
const imageUrl = await generateImage(prompt);
setImage(imageUrl);
} catch (error) {
Alert.alert('Error', error.message);
} finally {
setLoading(false);
}
};Example: Text Generation (Chat)
Edge Function
convex/functions/chat/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const { messages, model = 'gpt-4o-mini' } = await req.json();
const openaiKey = Deno.env.get('OPENAI_API_KEY');
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openaiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model,
messages,
temperature: 0.7,
max_tokens: 1000,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error?.message || 'OpenAI API error');
}
return new Response(
JSON.stringify({
message: data.choices[0].message.content,
usage: data.usage,
}),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});Example: Text-to-Speech (ElevenLabs)
Edge Function
convex/functions/text-to-speech/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const { text, voiceId = 'EXAVITQu4vr4xnSDxMaL' } = await req.json();
const elevenLabsKey = Deno.env.get('ELEVENLABS_API_KEY');
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: 'POST',
headers: {
'xi-api-key': elevenLabsKey!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
model_id: 'eleven_monolingual_v1',
voice_settings: {
stability: 0.5,
similarity_boost: 0.5,
},
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail?.message || 'ElevenLabs API error');
}
// Return audio as base64
const audioBuffer = await response.arrayBuffer();
const base64Audio = btoa(
String.fromCharCode(...new Uint8Array(audioBuffer))
);
return new Response(
JSON.stringify({ audio: base64Audio }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});Setting Secrets
Never commit API keys to git. Use Convex secrets:
# Set secrets (run once)
convex secrets set OPENAI_API_KEY=sk-...
convex secrets set ELEVENLABS_API_KEY=...
convex secrets set REPLICATE_API_TOKEN=r8_...
# List secrets
convex secrets listDeploying Functions
Deploy Single Function
convex functions deploy generate-imageDeploy All Functions
convex functions deployLink to Your Project
# Link to remote project
convex link --project-ref your-project-ref
# Deploy to production
convex functions deploy --project-ref your-project-refLocal Development
Start Local Convex
convex startServe Functions Locally
convex functions serveTest with cURL
curl -i --location --request POST \
'http://localhost:54321/functions/v1/generate-image' \
--header 'Authorization: Bearer YOUR_ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{"prompt":"a cute cat"}'Rate Limiting
Add rate limiting to protect your API costs:
convex/functions/_shared/rateLimit.ts
const rateLimits = new Map<string, { count: number; resetAt: number }>();
export function checkRateLimit(
userId: string,
maxRequests: number = 10,
windowMs: number = 60000
): boolean {
const now = Date.now();
const userLimit = rateLimits.get(userId);
if (!userLimit || now > userLimit.resetAt) {
rateLimits.set(userId, { count: 1, resetAt: now + windowMs });
return true;
}
if (userLimit.count >= maxRequests) {
return false;
}
userLimit.count++;
return true;
}In your function
import { checkRateLimit } from '../_shared/rateLimit.ts';
// After auth check
if (!checkRateLimit(user.id, 10, 60000)) {
throw new Error('Rate limit exceeded. Please try again later.');
}Credit System
Check user credits before expensive operations:
In your Edge Function
async function checkAndDeductCredits(
convex: ConvexClient,
userId: string,
creditsNeeded: number
): Promise<boolean> {
// Get current credits
const { data: user } = await convex
.from('user_credits')
.select('credits')
.eq('user_id', userId)
.single();
if (!user || user.credits < creditsNeeded) {
return false;
}
// Deduct credits
await convex
.from('user_credits')
.update({ credits: user.credits - creditsNeeded })
.eq('user_id', userId);
return true;
}
// Usage
const hasCredits = await checkAndDeductCredits(convexClient, user.id, 1);
if (!hasCredits) {
throw new Error('Insufficient credits. Please upgrade your plan.');
}Common Errors
| Error | Cause | Fix |
|---|---|---|
| FunctionsRelayError | Function crashed | Check function logs |
| FunctionsHttpError | Non-2xx response | Check error message |
| CORS error | Missing headers | Add corsHeaders |
| Unauthorized | Invalid/missing token | Check auth header |
View Logs
# Local
convex functions logs generate-image
# Production (in dashboard)
# Convex Dashboard → Edge Functions → LogsBest Practices
- Always validate input - Never trust client data
- Always check authentication - Verify user tokens
- Use environment variables - Never hardcode keys
- Add rate limiting - Protect against abuse
- Log errors - But never log sensitive data
- Return meaningful errors - Help debugging
- Set reasonable timeouts - Edge functions have 60s limit
Official Docs: convex.com/docs/guides/functions
10 Functions Included
Backend patterns already mapped out
Use Convex for the full backend path or Expo API Routes for secure server-side AI calls.
Convex actions
Expo API Routes
Secure API proxying
Get ShipReactNative
Save 40+ hours of setup