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 convex

2. Initialize in Your Project

convex init

This creates a convex/ folder:

convex/
├── config.toml
└── functions/

3. Create Your First Function

convex functions new generate-image

Example: 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 list

Deploying Functions

Deploy Single Function

convex functions deploy generate-image

Deploy All Functions

convex functions deploy

Link to Your Project

# Link to remote project
convex link --project-ref your-project-ref

# Deploy to production
convex functions deploy --project-ref your-project-ref

Local Development

Start Local Convex

convex start

Serve Functions Locally

convex functions serve

Test 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

ErrorCauseFix
FunctionsRelayErrorFunction crashedCheck function logs
FunctionsHttpErrorNon-2xx responseCheck error message
CORS errorMissing headersAdd corsHeaders
UnauthorizedInvalid/missing tokenCheck auth header

View Logs

# Local
convex functions logs generate-image

# Production (in dashboard)
# Convex Dashboard → Edge Functions → Logs

Best Practices

  1. Always validate input - Never trust client data
  2. Always check authentication - Verify user tokens
  3. Use environment variables - Never hardcode keys
  4. Add rate limiting - Protect against abuse
  5. Log errors - But never log sensitive data
  6. Return meaningful errors - Help debugging
  7. 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