> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Convex Setup

> Complete guide to setting up Convex as your backend

This guide walks you through setting up Convex as your backend provider. For Supabase setup, see the [Backend & Database](/core-features/backend) guide.

## Quick Start

### 1. Initialize Convex

From your project root:

```bash theme={null}
# 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

<Info>
  **First time?** You'll be prompted to log in to Convex and create a project.
</Info>

### 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:

```bash theme={null}
# 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.

<Accordion title="Manual setup (if CLI doesn't work)">
  If you have uncommitted changes or the CLI fails, generate keys manually:

  ```bash theme={null}
  # 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:

  ```bash theme={null}
  npx convex env set JWT_PRIVATE_KEY -- "<paste-private-key>"
  npx convex env set JWKS '<paste-jwks-json>'
  rm generate_keys.mjs
  ```
</Accordion>

<Warning>
  **Both `JWT_PRIVATE_KEY` and `JWKS` are required.** Missing either will cause sign-in to fail.
</Warning>

### 3. Set Backend Provider

Add to `apps/app/.env`:

```bash theme={null}
EXPO_PUBLIC_BACKEND_PROVIDER=convex
```

### 4. Add Seed Data

```bash theme={null}
npx convex run seed:run
```

This creates demo users, posts, and sample data for testing.

### 5. Run Your App

```bash theme={null}
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_KEY` | **Yes**   | RSA private key (PKCS#8 format) for signing auth tokens                   |
| `JWKS`            | **Yes**   | JSON Web Key Set (public key) for verifying tokens                        |
| `SITE_URL`        | For OAuth | Public URL for OAuth callbacks (e.g., `https://your-project.convex.site`) |

**Generate and set both keys:**

```bash theme={null}
npx @convex-dev/auth
```

Or see the [manual setup](#2-set-up-authentication-keys) in Quick Start above.

### OAuth Provider Variables

Set these in Convex Dashboard if using social login:

| Variable             | Provider                  |
| -------------------- | ------------------------- |
| `AUTH_GOOGLE_ID`     | Google OAuth              |
| `AUTH_GOOGLE_SECRET` | Google OAuth              |
| `AUTH_APPLE_ID`      | Apple Sign-In             |
| `AUTH_APPLE_SECRET`  | Apple Sign-In             |
| `AUTH_GITHUB_ID`     | GitHub OAuth              |
| `AUTH_GITHUB_SECRET` | GitHub OAuth              |
| `RESEND_API_KEY`     | Magic Link/OTP emails     |
| `RESEND_FROM_EMAIL`  | Magic Link sender address |
| `APP_NAME`           | App name shown in emails  |

### Client-Side Variables

These go in `apps/app/.env`:

| Variable                       | Description                |
| ------------------------------ | -------------------------- |
| `EXPO_PUBLIC_BACKEND_PROVIDER` | Set to `convex`            |
| `EXPO_PUBLIC_CONVEX_URL`       | Your Convex deployment URL |

### Useful Commands

```bash theme={null}
# 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    |

### Configure OAuth Providers

1. Go to [Convex Dashboard](https://dashboard.convex.dev)
2. Select your project → Settings → Environment Variables
3. Add the required credentials:

**Required for OAuth (all providers):**

```
SITE_URL=https://your-project.convex.site
```

<Warning>
  **`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](/core-features/social-login#mobile-oauth-requirements-convex) for details.
</Warning>

**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
```

<Warning>
  **Never put secrets in client code.** OAuth secrets must be set in the Convex Dashboard, not in your `.env` file.
</Warning>

<Tip>
  Check your current environment variables:

  ```bash theme={null}
  npx convex env list
  ```
</Tip>

***

## Using Auth in Your App

### The useAuth() Hook

Use `useAuth()` for **all** auth operations. It's the ONLY auth hook you need:

```typescript theme={null}
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>
  )
}
```

<Info>
  `useAuth()` works with both Supabase and Convex - all auth methods including magic link/OTP are supported on both backends.
</Info>

***

## Database Operations

### Queries (Reading Data)

```typescript theme={null}
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)

```typescript theme={null}
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`:

```typescript theme={null}
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

```bash theme={null}
# 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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

<Tip>
  Run `npx convex dev` to regenerate types after schema changes. Types are in `convex/_generated/`.
</Tip>

***

## Deployment

### Development

```bash theme={null}
npx convex dev  # Runs locally, auto-syncs changes
```

### Production

```bash theme={null}
npx convex deploy  # Deploy to production
```

### Environment Variables

Production secrets must be set in the Convex Dashboard:

1. Go to [dashboard.convex.dev](https://dashboard.convex.dev)
2. Select your project
3. Settings → Environment Variables
4. Add all OAuth credentials and API keys

***

## Troubleshooting

### "Missing environment variable JWT\_PRIVATE\_KEY", "JWKS", or "pkcs8 format" error

This means the auth keys are missing or incorrectly formatted. Run the Convex Auth setup:

```bash theme={null}
npx @convex-dev/auth
```

If you have uncommitted changes, see the [manual setup](#2-set-up-authentication-keys) instructions.

<Warning>
  * `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
</Warning>

### "Convex URL not configured" in app

Ensure `.env.local` has `EXPO_PUBLIC_CONVEX_URL` set, then restart Metro:

```bash theme={null}
yarn start --clear
```

### Auth not persisting

* Check SecureStore permissions (iOS/Android)
* Verify ConvexProvider wraps your app
* Check console for errors

### Types not updating

Regenerate types:

```bash theme={null}
npx convex dev  # Auto-regenerates _generated/
```

### Local backend not running

```bash theme={null}
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.

```bash theme={null}
# 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](/core-features/social-login#mobile-oauth-requirements-convex) for full details.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="lock" href="/core-features/authentication">
    Deep dive into auth flows
  </Card>

  <Card title="Realtime" icon="bolt" href="/core-features/realtime">
    Live updates and presence
  </Card>

  <Card title="Backend & Database" icon="database" href="/core-features/backend">
    Schema and security patterns
  </Card>

  <Card title="Deploy" icon="rocket" href="/deployment/index">
    Ship to production
  </Card>
</CardGroup>
