Build with the Convex API
Nimbus serves the Convex API from a single self-hosted binary: the same
convex/ project layout, the same function model, and the same client
packages — no hosted service required. This guide takes you from an empty
directory to working functions called from a client.
New to Nimbus entirely? Start with the developer quickstart. Migrating an existing Convex project? See Migrate a Convex app.
Prerequisites
Section titled “Prerequisites”- Nimbus installed (see the quickstart).
- Node.js 22 or newer with
npm. Nimbus runs codegen inside its own binary; Node is only needed to install your project’s npm dependencies.
1. Scaffold a project
Section titled “1. Scaffold a project”nimbus init convex my-appcd my-appThis creates backend files only: a schema, an example query and mutation,
package.json, tsconfig.json, and .gitignore. Bring your own frontend.
Pass --install to run npm install during init; otherwise nimbus dev
installs missing packages on first run. The full file list is in the
project layout reference.
2. Start the dev loop
Section titled “2. Start the dev loop”nimbus devThe dev server:
- serves on
http://localhost:3210and creates ademotenant, so your deployment URL ishttp://localhost:3210/convex/demo; - watches your source files, re-runs codegen on change, and activates the updated functions — reactive subscriptions pick up the new results;
- stores its data under
.nimbus/dev/in your project.
Pass --port to change the port and --once to start without watching.
3. Define a schema
Section titled “3. Define a schema”import { defineSchema, defineTable } from "convex/server";import { v } from "convex/values";
export default defineSchema({ messages: defineTable({ author: v.string(), body: v.string(), }).index("by_author", ["author"]),});The schema file is optional: a table without a schema accepts documents of any shape. Adding a schema enforces the declared fields and makes the generated types precise. The supported validators are listed in the compatibility reference.
4. Write functions
Section titled “4. Write functions”Register functions with query, mutation, and action from
./_generated/server. Always declare argument validators.
import { v } from "convex/values";import { query, mutation } from "./_generated/server";
export const list = query({ args: {}, handler: async (ctx) => await ctx.db.query("messages").take(50),});
export const send = mutation({ args: { author: v.string(), body: v.string() }, handler: async (ctx, { author, body }) => await ctx.db.insert("messages", { author, body }),});Use an index instead of a filter when selecting rows:
export const byAuthor = query({ args: { author: v.string() }, handler: async (ctx, { author }) => await ctx.db .query("messages") .withIndex("by_author", (q) => q.eq("author", author)) .collect(),});Each function kind gets a different ctx:
- Queries read the database (
ctx.db.query,ctx.db.get). - Mutations read and write (
ctx.db.insert,ctx.db.patch,ctx.db.delete) and can schedule work withctx.scheduler. - Actions have no database access; they call other functions through
ctx.runQuery,ctx.runMutation, andctx.runAction, and can use"use node"modules for npm packages.
Prefix any of them with internal (internalQuery, internalMutation,
internalAction) to keep a function callable only from other functions,
never from clients. HTTP endpoints go in convex/http.ts with httpAction
and httpRouter. The full rule set lives in the
usage rules reference.
5. Call functions from a client
Section titled “5. Call functions from a client”Function references come from the generated api object. From React:
import { ConvexProvider, ConvexReactClient, useQuery } from "convex/react";import { api } from "../convex/_generated/api";
const client = new ConvexReactClient("http://localhost:3210/convex/demo");
function Messages() { const messages = useQuery(api.messages.list, {}); return <ul>{messages?.map((m) => <li key={m._id}>{m.body}</li>)}</ul>;}From Node.js or any server-side script:
import { ConvexHttpClient } from "convex/browser";import { api } from "./convex/_generated/api.js";
const client = new ConvexHttpClient("http://localhost:3210/convex/demo");const messages = await client.query(api.messages.list, {});Queries are reactive over the WebSocket clients; mutations and actions are
invoked the same way with useMutation, useAction, or client.mutation.
Next steps
Section titled “Next steps”- Migrate a Convex app — bring an existing project over.
- Project layout — every file
nimbus init convexcreates and what codegen generates. - Compatibility — the supported API surface, validator set, and known gaps.
- Usage rules — the contract for writing functions that run correctly on Nimbus.