This guide walks you through setting up Convex as your backend provider. For Supabase setup, see the Backend & Database guide.
Quick Start
1. Initialize Convex
From your project root:
# Start the Convex development server
npx convex dev
This will:
Create a new Convex project (or link to existing)
Save CONVEX_DEPLOYMENT to .env.local
Save EXPO_PUBLIC_CONVEX_URL to .env.local
Start watching for changes
First time? You’ll be prompted to log in to Convex and create a project.
2. Set Up Authentication Keys
Convex Auth requires JWT_PRIVATE_KEY and JWKS environment variables for signing and verifying tokens. The easiest way to set them up:
# Generate both keys using the Convex Auth CLI
npx @convex-dev/auth
This will prompt you to generate and set both JWT_PRIVATE_KEY and JWKS automatically.
Manual setup (if CLI doesn't work)
If you have uncommitted changes or the CLI fails, generate keys manually: # Create a script to generate matching keys
cat << 'EOF' > generate_keys.mjs
import crypto from 'crypto';
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const publicKeyObj = crypto.createPublicKey(publicKey);
const jwk = publicKeyObj.export({ format: 'jwk' });
jwk.alg = 'RS256';
jwk.use = 'sig';
jwk.kid = crypto.randomUUID();
console.log('=== JWT_PRIVATE_KEY ===');
console.log(privateKey);
console.log('=== JWKS ===');
console.log(JSON.stringify({ keys: [jwk] }));
EOF
node generate_keys.mjs
Then set both values in Convex: npx convex env set JWT_PRIVATE_KEY -- "<paste-private-key>"
npx convex env set JWKS '<paste-jwks-json>'
rm generate_keys.mjs
Both JWT_PRIVATE_KEY and JWKS are required. Missing either will cause sign-in to fail.
3. Set Backend Provider
Add to apps/app/.env:
EXPO_PUBLIC_BACKEND_PROVIDER = convex
4. Add Seed Data
This creates demo users, posts, and sample data for testing.
5. Run Your App
cd apps/app
yarn ios # or yarn android, yarn web
Project Structure
your-project/
├── convex/ # Convex backend
│ ├── _generated/ # Auto-generated types (don't edit)
│ ├── auth.ts # Auth provider configuration
│ ├── schema.ts # Database schema
│ ├── seed.ts # Seed data utilities
│ ├── realtime.ts # Presence & broadcast functions
│ ├── users.ts # User-related functions
│ ├── storage.ts # File storage functions
│ ├── crons.ts # Scheduled jobs
│ └── lib/
│ └── security.ts # Auth helpers (requireAuth, etc.)
└── apps/app/
└── app/
├── hooks/convex/ # Convex-specific hooks
├── providers/ # ConvexProvider setup
└── services/backend/ # Backend abstraction layer
Environment Variables
Convex uses server-side environment variables (set in Convex Dashboard) and client-side variables (in your .env file).
Required Server-Side Variables
These must be set in the Convex Dashboard → Settings → Environment Variables:
Variable Required Description JWT_PRIVATE_KEYYes RSA private key (PKCS#8 format) for signing auth tokens JWKSYes JSON Web Key Set (public key) for verifying tokens SITE_URLFor OAuth Public URL for OAuth callbacks (e.g., https://your-project.convex.site)
Generate and set both keys:
Or see the manual setup in Quick Start above.
OAuth Provider Variables
Set these in Convex Dashboard if using social login:
Variable Provider AUTH_GOOGLE_IDGoogle OAuth AUTH_GOOGLE_SECRETGoogle OAuth AUTH_APPLE_IDApple Sign-In AUTH_APPLE_SECRETApple Sign-In AUTH_GITHUB_IDGitHub OAuth AUTH_GITHUB_SECRETGitHub OAuth RESEND_API_KEYMagic Link/OTP emails RESEND_FROM_EMAILMagic Link sender address APP_NAMEApp name shown in emails
Client-Side Variables
These go in apps/app/.env:
Variable Description EXPO_PUBLIC_BACKEND_PROVIDERSet to convex EXPO_PUBLIC_CONVEX_URLYour Convex deployment URL
Useful Commands
# List all environment variables
npx convex env list
# Set a variable
npx convex env set VARIABLE_NAME "value"
# Remove a variable
npx convex env unset VARIABLE_NAME
Authentication Setup
The boilerplate includes pre-configured auth providers in convex/auth.ts:
Available Providers
Provider Usage Requirements Password signIn('password', { email, password })None Google signIn('google')OAuth credentials Apple signIn('apple')OAuth credentials GitHub signIn('github')OAuth credentials Email/OTP signIn('resend', { email })Resend API key
Go to Convex Dashboard
Select your project → Settings → Environment Variables
Add the required credentials:
Required for OAuth (all providers):
SITE_URL=https://your-project.convex.site
SITE_URL must be a public URL for mobile OAuth. OAuth won’t work on iOS/Android simulators if SITE_URL is set to localhost or 127.0.0.1. See Mobile OAuth Requirements for details.
Google OAuth:
AUTH_GOOGLE_ID=your-client-id
AUTH_GOOGLE_SECRET=your-client-secret
Apple Sign-In:
AUTH_APPLE_ID=your-services-id
AUTH_APPLE_SECRET=your-client-secret-jwt
GitHub OAuth:
AUTH_GITHUB_ID=your-client-id
AUTH_GITHUB_SECRET=your-client-secret
Magic Link / OTP (via Resend):
RESEND_API_KEY=your-resend-api-key
RESEND_FROM_EMAIL=noreply@yourdomain.com
APP_NAME=YourAppName
Never put secrets in client code. OAuth secrets must be set in the Convex Dashboard, not in your .env file.
Check your current environment variables:
Using Auth in Your App
The useAuth() Hook
Use useAuth() for all auth operations. It’s the ONLY auth hook you need:
import { useAuth } from '@/hooks'
function ProfileScreen () {
const {
isAuthenticated ,
isLoading ,
user ,
signIn ,
signUp ,
signOut ,
signInWithGoogle ,
signInWithApple ,
signInWithMagicLink ,
verifyOtp ,
updateProfile ,
} = useAuth ()
if ( isLoading ) return < Loading />
if ( ! isAuthenticated ) {
return (
< View >
< Button
onPress = { signInWithGoogle }
title = "Sign in with Google"
/>
< Button
onPress = { signInWithApple }
title = "Sign in with Apple"
/>
</ View >
)
}
return (
< View >
< Text > Welcome , { user ?. displayName } </ Text >
< Button onPress = { signOut } title = "Sign Out" />
</ View >
)
}
useAuth() works with both Supabase and Convex - all auth methods including magic link/OTP are supported on both backends.
Database Operations
Queries (Reading Data)
import { useQuery } from '@/hooks'
import { api } from '@convex/_generated/api'
function PostList () {
// Automatically reactive - updates when data changes
const posts = useQuery ( api . posts . list )
if ( posts === undefined ) return < Loading />
return posts.map( post => < PostCard key ={ post . _id } post ={ post } />)
}
Mutations (Writing Data)
import { useMutation } from '@/hooks'
import { api } from '@convex/_generated/api'
function CreatePost () {
const createPost = useMutation ( api . posts . create )
const handleSubmit = async () => {
await createPost ({
title: 'My Post' ,
content: 'Hello world!' ,
})
}
return < Button onPress = { handleSubmit } title = "Create" />
}
Security
Convex uses function-level security. Always use the security helpers in convex/lib/security.ts:
import { requireAuth , requireOwnership } from "./lib/security"
export const updatePost = mutation ({
args: { postId: v . id ( "posts" ), title: v . string () },
handler : async ( ctx , args ) => {
// Throws if not authenticated
const userId = await requireAuth ( ctx )
// Throws if user doesn't own this post
await requireOwnership ( ctx , "posts" , args . postId , userId , "authorId" )
// Safe to update
await ctx . db . patch ( args . postId , { title: args . title })
},
})
Available Helpers
Helper Description requireAuth(ctx)Throws if not authenticated, returns userId getAuthUserId(ctx)Returns userId or null requireOwnership(ctx, table, id, userId, field)Throws if user doesn’t own resource requireRole(ctx, userId, roles[])Throws if user lacks required role auditLog(ctx, userId, action, metadata)Log security-relevant actions
Seed Data
Commands
# Populate demo data
npx convex run seed:run
# Check database status
npx convex run seed:status
# Clear all seed data
npx convex run seed:clear
# Add a test user
npx convex run seed:addTestUser
Demo Data Included
2 demo users : demo@shipnative.app, test@shipnative.app
3 sample posts : Published and draft examples
Sample comments : With threading
Welcome notifications : For each user
Realtime Features
Convex queries are reactive by default. The boilerplate also includes explicit presence and broadcast functions:
Presence Tracking
import { useMutation , useQuery } from '@/hooks'
import { api } from '@convex/_generated/api'
function CollaborativeDoc ({ docId }) {
const joinPresence = useMutation ( api . realtime . joinPresence )
const presence = useQuery ( api . realtime . getPresence , {
channel: `doc: ${ docId } `
})
useEffect (() => {
joinPresence ({ channel: `doc: ${ docId } ` , state: { status: 'viewing' } })
return () => leavePresence ({ channel: `doc: ${ docId } ` })
}, [ docId ])
return < Text >{presence?. count } users online </ Text >
}
Broadcast Messages
const broadcast = useMutation ( api . realtime . broadcast )
// Send cursor position to all users
broadcast ({
channel: `doc: ${ docId } ` ,
event: 'cursor_move' ,
payload: { x: 100 , y: 200 }
})
File Storage
Convex provides built-in file storage. Upload files directly to Convex and get URLs for display.
Upload Files
import { useMutation } from '@/hooks'
import { api } from '@convex/_generated/api'
function FileUploader () {
const generateUploadUrl = useMutation ( api . storage . generateUploadUrl )
const saveFile = useMutation ( api . storage . saveFile )
const uploadFile = async ( file : File ) => {
// 1. Get upload URL from Convex
const uploadUrl = await generateUploadUrl ()
// 2. Upload file directly to Convex storage
const response = await fetch ( uploadUrl , {
method: 'POST' ,
headers: { 'Content-Type' : file . type },
body: file ,
})
const { storageId } = await response . json ()
// 3. Save metadata to database
await saveFile ({
storageId ,
filename: file . name ,
contentType: file . type ,
size: file . size ,
})
}
return < Button onPress = {() => pickAndUpload ()} title = "Upload" />
}
Get File URLs
import { useQuery } from '@/hooks'
import { api } from '@convex/_generated/api'
function FileList () {
const files = useQuery ( api . storage . listMyFiles )
return (
< FlatList
data = { files }
renderItem = {({ item }) => (
<View>
<Text>{ item . filename }</Text>
<Image source = {{ uri : item . url }} />
</View>
)}
/>
)
}
Cron Jobs
Convex supports scheduled tasks via cron jobs defined in convex/crons.ts.
Built-in Jobs
Job Interval Description Cleanup stale presence 1 minute Removes presence records older than 1 minute Cleanup expired broadcasts 5 minutes Removes broadcast messages past TTL
Adding Custom Crons
// In convex/crons.ts
import { cronJobs } from "convex/server"
import { internal } from "./_generated/api"
const crons = cronJobs ()
// Run every hour
crons . interval (
"my custom job" ,
{ hours: 1 },
internal . myModule . myFunction ,
{ arg1: "value" }
)
// Run daily at midnight UTC
crons . daily (
"daily cleanup" ,
{ hourUTC: 0 , minuteUTC: 0 },
internal . cleanup . runDaily
)
export default crons
Type Safety
Convex provides end-to-end type safety from your schema to your React components.
How It Works
// 1. Schema defines types
const schema = defineSchema ({
posts: defineTable ({
title: v . string (),
views: v . number (),
}),
})
// 2. Generated types flow through automatically
const posts = useQuery ( api . posts . list )
// ^? Post[] | undefined
// 3. TypeScript catches errors at compile time
const createPost = useMutation ( api . posts . create )
await createPost ({ title: 123 }) // ❌ Error: number not assignable to string
await createPost ({ title: "Hello" }) // ✅ Works
Benefits
No manual type definitions - types are generated from your schema
Autocomplete everywhere - IDE knows all available fields
Catch errors early - TypeScript errors before you run the code
Refactoring safety - rename a field and see all usages
Run npx convex dev to regenerate types after schema changes. Types are in convex/_generated/.
Deployment
Development
npx convex dev # Runs locally, auto-syncs changes
Production
npx convex deploy # Deploy to production
Environment Variables
Production secrets must be set in the Convex Dashboard:
Go to dashboard.convex.dev
Select your project
Settings → Environment Variables
Add all OAuth credentials and API keys
Troubleshooting
This means the auth keys are missing or incorrectly formatted. Run the Convex Auth setup:
If you have uncommitted changes, see the manual setup instructions.
JWT_PRIVATE_KEY must be in PKCS#8 PEM format (starts with -----BEGIN PRIVATE KEY-----)
JWKS must be valid JSON with a keys array containing the matching public key
Both must be generated together from the same key pair
Ensure .env.local has EXPO_PUBLIC_CONVEX_URL set, then restart Metro:
Auth not persisting
Check SecureStore permissions (iOS/Android)
Verify ConvexProvider wraps your app
Check console for errors
Types not updating
Regenerate types:
npx convex dev # Auto-regenerates _generated/
Local backend not running
npx convex dev # Must be running in a terminal
OAuth not working on mobile
OAuth requires a publicly accessible SITE_URL. If you’re using local Convex (127.0.0.1), OAuth will show “Convex Auth is running” instead of redirecting to the OAuth provider.
# Check your SITE_URL
npx convex env list
# Deploy to cloud and set public URL
npx convex deploy
npx convex env set SITE_URL "https://your-project.convex.site"
See Social Login - Mobile OAuth Requirements for full details.
Next Steps