Claude Code for Convex: Real-Time Backend-as-a-Service with TypeScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for Convex: Real-Time Backend-as-a-Service with TypeScript
Backend

Claude Code for Convex: Real-Time Backend-as-a-Service with TypeScript

Published: January 21, 2027
Read time: 8 min read
By: Claude Skills 360

Convex is a reactive backend platform: define schema, queries, and mutations in TypeScript, and React components subscribe to live data with useQuery — updates flow automatically when the database changes. Queries are deterministic functions that read from the database. Mutations transactionally write data. Actions call external APIs and schedule work. The schema enforces types at runtime. Every query and mutation is an isolated transaction on Convex’s built-in document database. Claude Code generates Convex schema definitions, query and mutation functions, React hooks integration, scheduled actions, and the file storage patterns for production Convex applications.

CLAUDE.md for Convex Projects

## Convex Stack
- Version: convex >= 1.15
- Schema: define in convex/schema.ts with v.* validators — required for all tables
- Functions: query (read), mutation (write), action (external I/O) — all server-side TypeScript
- React: useQuery() for live data, useMutation() for writes, useAction() for actions
- Auth: convex-auth with GitHub/Google/password, or custom JWT via middleware
- Files: storage.generateUploadUrl() + storage.getUrl() + ctx.storage.store()
- Scheduled: api.myModule.myAction via ctx.scheduler.runAfter/runAt

Schema Definition

// convex/schema.ts — type-safe database schema
import { defineSchema, defineTable } from "convex/server"
import { v } from "convex/values"

export default defineSchema({
  orders: defineTable({
    customerId: v.string(),
    status: v.union(
      v.literal("pending"),
      v.literal("processing"),
      v.literal("shipped"),
      v.literal("delivered"),
      v.literal("cancelled"),
    ),
    totalCents: v.number(),
    items: v.array(v.object({
      productId: v.string(),
      productName: v.string(),
      quantity: v.number(),
      priceCents: v.number(),
    })),
    shippingAddress: v.optional(v.object({
      line1: v.string(),
      city: v.string(),
      country: v.string(),
      postalCode: v.string(),
    })),
    trackingNumber: v.optional(v.string()),
    createdBy: v.id("users"),
  })
    .index("by_customer", ["customerId"])
    .index("by_status", ["status"])
    .searchIndex("search_orders", {
      searchField: "customerId",
      filterFields: ["status"],
    }),

  users: defineTable({
    name: v.string(),
    email: v.string(),
    role: v.union(v.literal("customer"), v.literal("admin")),
    tokenIdentifier: v.string(),
  }).index("by_token", ["tokenIdentifier"]),
})

Query Functions

// convex/orders.ts — queries, mutations, and actions
import { query, mutation, action } from "./_generated/server"
import { v } from "convex/values"
import { paginationOptsValidator } from "convex/server"

// Query: read-only, auto-subscribes in React
export const listOrders = query({
  args: {
    status: v.optional(v.string()),
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error("Unauthorized")

    const user = await ctx.db
      .query("users")
      .withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
      .unique()

    if (!user) throw new Error("User not found")

    let ordersQuery = ctx.db
      .query("orders")
      .withIndex("by_customer", (q) => q.eq("customerId", user._id))

    if (args.status) {
      ordersQuery = ctx.db
        .query("orders")
        .withIndex("by_status", (q) => q.eq("status", args.status!))
    }

    return await ordersQuery
      .order("desc")
      .paginate(args.paginationOpts)
  },
})

// Get single order
export const getOrder = query({
  args: { orderId: v.id("orders") },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error("Unauthorized")

    const order = await ctx.db.get(args.orderId)
    if (!order) throw new Error("Order not found")

    return order
  },
})

// Admin query: all orders with customer details
export const adminListOrders = query({
  args: {
    status: v.optional(v.string()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity?.tokenIdentifier) throw new Error("Unauthorized")

    const user = await ctx.db
      .query("users")
      .withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
      .unique()

    if (user?.role !== "admin") throw new Error("Forbidden")

    const limit = args.limit ?? 50

    let ordersQuery = ctx.db.query("orders").order("desc")
    const orders = await ordersQuery.take(limit)

    // Eager load customers
    const customerIds = [...new Set(orders.map((o) => o.customerId))]
    const customers = await Promise.all(customerIds.map((id) => ctx.db.get(id as any)))
    const customerMap = Object.fromEntries(
      customers.filter(Boolean).map((c) => [c!._id, c])
    )

    return orders.map((order) => ({
      ...order,
      customer: customerMap[order.customerId],
    }))
  },
})

Mutation Functions

// Mutation: transactional write
export const createOrder = mutation({
  args: {
    items: v.array(v.object({
      productId: v.string(),
      productName: v.string(),
      quantity: v.number(),
      priceCents: v.number(),
    })),
    shippingAddress: v.object({
      line1: v.string(),
      city: v.string(),
      country: v.string(),
      postalCode: v.string(),
    }),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error("Unauthorized")

    const user = await ctx.db
      .query("users")
      .withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
      .unique()

    if (!user) throw new Error("User not found")

    const totalCents = args.items.reduce(
      (sum, item) => sum + item.priceCents * item.quantity,
      0
    )

    const orderId = await ctx.db.insert("orders", {
      customerId: user._id,
      status: "pending",
      totalCents,
      items: args.items,
      shippingAddress: args.shippingAddress,
      createdBy: user._id,
    })

    // Schedule fulfillment action after 1 second
    await ctx.scheduler.runAfter(1000, api.orders.startFulfillment, {
      orderId,
    })

    return orderId
  },
})

export const cancelOrder = mutation({
  args: { orderId: v.id("orders") },
  handler: async (ctx, args) => {
    const order = await ctx.db.get(args.orderId)
    if (!order) throw new Error("Order not found")
    if (order.status !== "pending") {
      throw new Error(`Cannot cancel order with status: ${order.status}`)
    }

    await ctx.db.patch(args.orderId, { status: "cancelled" })
    return args.orderId
  },
})

Action Functions and Scheduling

// Actions: can call external APIs, run side effects
import { action, internalMutation } from "./_generated/server"
import { api, internal } from "./_generated/api"

export const startFulfillment = action({
  args: { orderId: v.id("orders") },
  handler: async (ctx, args) => {
    // Call external fulfillment API
    const response = await fetch("https://fulfillment.api.com/orders", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.FULFILLMENT_API_KEY}`,
      },
      body: JSON.stringify({ orderId: args.orderId }),
    })

    if (!response.ok) {
      throw new Error(`Fulfillment API error: ${response.status}`)
    }

    const { fulfillmentId } = await response.json()

    // Update database via internal mutation
    await ctx.runMutation(internal.orders.updateFulfillmentId, {
      orderId: args.orderId,
      fulfillmentId,
    })
  },
})

// Scheduled function: daily revenue report
export const sendDailyReport = action({
  args: {},
  handler: async (ctx) => {
    const orders = await ctx.runQuery(api.orders.adminListOrders, {
      status: "delivered",
      limit: 1000,
    })

    const revenue = orders.reduce((sum: number, o: any) => sum + o.totalCents, 0)

    await sendSlackMessage(`Daily Revenue: $${(revenue / 100).toFixed(2)} from ${orders.length} orders`)
  },
})

React Integration

// src/components/OrderList.tsx — live-updating React with Convex
import { useQuery, useMutation } from "convex/react"
import { api } from "../convex/_generated/api"
import { usePaginatedQuery } from "convex/react"

export function OrderList() {
  // useQuery: subscribes to real-time updates
  const { results: orders, loadMore, status } = usePaginatedQuery(
    api.orders.listOrders,
    {},  // args
    { initialNumItems: 20 },
  )

  const cancelOrder = useMutation(api.orders.cancelOrder)

  const handleCancel = async (orderId: string) => {
    try {
      await cancelOrder({ orderId: orderId as any })
    } catch (e) {
      alert(`Failed to cancel: ${(e as Error).message}`)
    }
  }

  if (status === "LoadingFirstPage") return <div>Loading...</div>

  return (
    <div className="space-y-3">
      {orders.map((order) => (
        <div key={order._id} className="border rounded-lg p-4">
          <div className="flex justify-between">
            <div>
              <p className="font-mono text-sm">{order._id}</p>
              <p className="font-bold">${(order.totalCents / 100).toFixed(2)}</p>
            </div>
            <div className="flex items-center gap-2">
              <span className="text-sm">{order.status}</span>
              {order.status === "pending" && (
                <button
                  onClick={() => handleCancel(order._id)}
                  className="text-red-500 text-sm"
                >
                  Cancel
                </button>
              )}
            </div>
          </div>
        </div>
      ))}

      {status === "CanLoadMore" && (
        <button onClick={() => loadMore(20)}>Load more</button>
      )}
    </div>
  )
}

For the Supabase alternative with PostgreSQL + Row Level Security + realtime subscriptions instead of Convex’s document database, see the patterns in the authentication guide for Supabase Auth. For the tRPC + Prisma stack that provides similar TypeScript end-to-end type safety with your own PostgreSQL database, the Prisma advanced guide covers extensions and transactions. The Claude Skills 360 bundle includes Convex skill sets covering schema design, query/mutation patterns, and React integration. Start with the free tier to try Convex backend generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free