Convex Backend
What We Did
Section titled “What We Did”- Added Convex to
packages/backendas the application’s backend-as-a-service solution - Created a Claude Code skill (
/convex) for AI-assisted Convex development - Integrated Convex with the Next.js web app with a working “Things” CRUD example
Why Convex
Section titled “Why Convex”Convex provides a complete backend solution that aligns perfectly with our Bun-first, TypeScript-native approach.
Key reasons:
- Real-time by default: Live queries automatically update when data changes
- TypeScript-first: End-to-end type safety from database to client
- Serverless: No infrastructure to manage, scales automatically
- Developer experience: Hot reload, local dev server, built-in debugging
- React integration: First-class hooks (
useQuery,useMutation)
Alternatives considered:
- Supabase: Excellent, but Convex’s real-time model is simpler
- Firebase: Good real-time, but TypeScript support is weaker
- tRPC + Prisma: Type-safe, but more complex setup and no built-in real-time
Project Structure
Section titled “Project Structure”packages/backend/├── convex/│ ├── _generated/ # Auto-generated (don't edit)│ │ ├── api.d.ts│ │ ├── api.js│ │ ├── dataModel.d.ts│ │ └── server.ts│ ├── schema.ts # Database schema│ ├── things.ts # Things queries/mutations│ └── tsconfig.json├── .env.local # Convex deployment URL (gitignored)├── .env.example # Template for .env.local└── package.json
apps/web/├── app/│ ├── providers.tsx # ConvexProvider setup│ ├── layout.tsx # Wraps app with Providers│ └── page.tsx # Things CRUD UI├── .env.local # Copy of backend .env.local + NEXT_PUBLIC_*└── .env.exampleBackend Implementation
Section titled “Backend Implementation”Schema (convex/schema.ts)
Section titled “Schema (convex/schema.ts)”import { defineSchema, defineTable } from "convex/server"import { v } from "convex/values"
export default defineSchema({ things: defineTable({ title: v.string(), }),})Queries and Mutations (convex/things.ts)
Section titled “Queries and Mutations (convex/things.ts)”import { v } from "convex/values"
import { mutation, query } from "./_generated/server"
export const getThings = query({ args: {}, handler: async (ctx) => { return await ctx.db.query("things").collect() },})
export const getThing = query({ args: { id: v.id("things"), }, handler: async (ctx, args) => { return await ctx.db.get(args.id) },})
export const createThing = mutation({ args: { title: v.string(), }, handler: async (ctx, args) => { return await ctx.db.insert("things", { title: args.title }) },})Package Exports (package.json)
Section titled “Package Exports (package.json)”The backend package exports the Convex API for the web app:
{ "name": "backend", "exports": { "./convex": "./convex/_generated/api.js" }, "dependencies": { "convex": "^1.31.6" }}Frontend Integration
Section titled “Frontend Integration”Convex Provider (apps/web/app/providers.tsx)
Section titled “Convex Provider (apps/web/app/providers.tsx)”"use client"
import { ReactNode } from "react"
import { ConvexProvider, ConvexReactClient } from "convex/react"
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
export function Providers({ children }: { children: ReactNode }) { if (!convexUrl) { return ( <div style={{ padding: "2rem", fontFamily: "system-ui" }}> <h1>Convex Not Configured</h1> <p> Missing <code>NEXT_PUBLIC_CONVEX_URL</code> environment variable. </p> {/* Setup instructions... */} </div> ) }
const convex = new ConvexReactClient(convexUrl) return <ConvexProvider client={convex}>{children}</ConvexProvider>}Layout Integration (apps/web/app/layout.tsx)
Section titled “Layout Integration (apps/web/app/layout.tsx)”import { Providers } from "./providers"
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> )}Using Convex in Components (apps/web/app/page.tsx)
Section titled “Using Convex in Components (apps/web/app/page.tsx)”"use client"
import { FormEvent, useState } from "react"
import { api } from "backend/convex"import { useMutation, useQuery } from "convex/react"
export default function Home() { const things = useQuery(api.things.getThings) const createThing = useMutation(api.things.createThing) const [title, setTitle] = useState("")
const handleSubmit = async (e: FormEvent) => { e.preventDefault() if (!title.trim()) return await createThing({ title: title.trim() }) setTitle("") }
return ( <main> <h1>Things Manager</h1>
{/* Create Form */} <form onSubmit={handleSubmit}> <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Enter thing title..." /> <button type="submit">Create</button> </form>
{/* List Things */} {things === undefined ? ( <p>Loading...</p> ) : things.length === 0 ? ( <p>No things yet.</p> ) : ( <ul> {things.map((thing) => ( <li key={thing._id}> {thing.title} <span>{new Date(thing._creationTime).toLocaleDateString()}</span> </li> ))} </ul> )} </main> )}Environment Variables
Section titled “Environment Variables”Turborepo Configuration (turbo.json)
Section titled “Turborepo Configuration (turbo.json)”Declare Convex env vars for proper cache invalidation:
{ "globalEnv": ["NEXT_PUBLIC_CONVEX_URL", "CONVEX_DEPLOYMENT"]}Backend .env.local
Section titled “Backend .env.local”Generated by bunx convex dev:
CONVEX_DEPLOYMENT=dev:your-deployment-nameCONVEX_URL=https://your-deployment-name.convex.cloudWeb App .env.local
Section titled “Web App .env.local”Copy from backend and add NEXT_PUBLIC_ prefix:
CONVEX_DEPLOYMENT=dev:your-deployment-nameCONVEX_URL=https://your-deployment-name.convex.cloudNEXT_PUBLIC_CONVEX_URL=https://your-deployment-name.convex.cloudImportant: Both files must point to the same Convex deployment.
Claude Code Skill
Section titled “Claude Code Skill”A comprehensive /convex skill was added to .claude/skills/convex/SKILL.md providing:
- Function syntax: Queries, mutations, actions with validators
- Schema design: Tables, indexes, validators
- TypeScript patterns:
Id<"tableName">, strict typing - Best practices: Use indexes over
.filter(), include return validators - Complete examples: Real-world chat app implementation
Usage: Invoke /convex when writing Convex code for AI-assisted development.
Development Workflow
Section titled “Development Workflow”Initial Setup
Section titled “Initial Setup”# 1. Start Convex dev server (generates .env.local)cd packages/backendbunx convex dev
# 2. Copy env to web appcp .env.local ../../apps/web/.env.local
# 3. Add NEXT_PUBLIC prefix to web app's .env.localecho "NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud" >> ../../apps/web/.env.local
# 4. Start all appscd ../..bun devOngoing Development
Section titled “Ongoing Development”Run both servers in parallel (Turborepo handles this):
bun dev# Runs: web#dev, docs#dev, backend#dev (convex dev)Or run individually:
turbo dev --filter=backend # Convex dev serverturbo dev --filter=web # Next.js on port 3000Key Patterns
Section titled “Key Patterns”Importing the API
Section titled “Importing the API”// In web app componentsimport { api } from "backend/convex"
// Use with hooksconst data = useQuery(api.things.getThings)const mutate = useMutation(api.things.createThing)Reactive Queries
Section titled “Reactive Queries”Convex queries are reactive - the UI automatically updates when data changes:
const things = useQuery(api.things.getThings)// No manual refetching needed - updates automaticallyType Safety
Section titled “Type Safety”Full end-to-end type inference:
// Backend defines the shapeexport const createThing = mutation({ args: { title: v.string() }, handler: async (ctx, args) => { /* ... */ },})
// Frontend gets type checkingcreateThing({ title: "Hello" }) // ✓createThing({ name: "Hello" }) // ✗ Type errorTesting/Verification
Section titled “Testing/Verification”# Verify Convex is runningcd packages/backend && bunx convex dev# Should show: "Convex functions ready!"
# Verify web app connectsturbo dev --filter=web# Open http://localhost:3000# Should show Things Manager UI
# Test the integration# 1. Enter a title and click Create# 2. New thing appears instantly (real-time)# 3. Open Convex dashboard to see dataTroubleshooting
Section titled “Troubleshooting”| Issue | Solution |
|---|---|
| ”No address provided to ConvexReactClient” | Missing NEXT_PUBLIC_CONVEX_URL in apps/web/.env.local |
| ”Could not find public function” | Different deployments - sync .env.local files |
| Functions not updating | Restart bunx convex dev |
| Types not found | Run bun install at repo root |
Related Documentation
Section titled “Related Documentation”- Convex Documentation
- Convex React Integration
- Convex TypeScript Guide
/convexskill in.claude/skills/convex/SKILL.md