Quickstart
Scaffold a Next.js app with kitcn, bootstrap a local backend, and understand the starter messages demo.
In this guide, we'll take the fastest happy path: scaffold a fresh Next.js app with the CLI, run the first local Convex bootstrap in the same command, and walk through the starter messages demo from schema to client.
If you want the full command map, start with /docs/cli/registry. This guide stays on the shortest path.
Docs use the ORM (ctx.orm) everywhere. The scaffolded starter demo keeps the
backend tiny on purpose, so it uses direct Convex reads and writes. Once the
app is running, you can move deeper into the ORM guides and migration docs.
1. Scaffold A Fresh App
Let's start with an empty directory and let the CLI own the bootstrap.
mkdir my-app
cd my-app
bunx kitcn init -t next --yesinit -t next creates the Next.js shell and layers in the kitcn
baseline in one pass. That includes the backend files, client providers, local
env scaffolding, and a tiny messages demo so you have something real to run
immediately.
Here are the key files the quickstart uses:
| File | Purpose |
|---|---|
convex/functions/schema.ts | Starter schema |
convex/functions/messages.ts | Starter query and mutation |
convex/lib/crpc.ts | Server-side cRPC builders |
lib/convex/crpc.tsx | React cRPC context |
components/providers.tsx | App-level providers |
app/convex/page.tsx | Starter demo page |
If you already have a supported app shell, use kitcn init --yes
instead. That adoption path is documented in
/docs/cli/registry.
2. Run The App
Now let's start the backend for real and open the Next.js app in a second terminal.
bunx kitcn devbun devOpen http://localhost:3000/convex. You should see the starter messages page. Add a message and watch the list update.
That gives you the full end-to-end loop:
- query from React
- mutation from React
- live backend round-trip
- generated API metadata through
@convex/api
If the page says the backend is not ready, check the terminal running
kitcn dev. The starter page already includes that empty/error state,
so you do not have to guess what failed.
3. Read The Starter Backend
Now that the app is running, let's trace the files you just bootstrapped.
Schema
We'll start with the starter schema. It is intentionally tiny.
import { convexTable, defineSchema, text } from 'kitcn/orm';
export const messagesTable = convexTable('messages', {
body: text().notNull(),
});
export const tables = {
messages: messagesTable,
};
export default defineSchema(tables);This gives the demo one messages table with a single required body field.
That is enough to prove the full query and mutation loop without burying the
quickstart in schema noise.
Procedures
Next, let's look at the starter procedures.
import { z } from 'zod';
import { publicMutation, publicQuery } from '../lib/crpc';
export const list = publicQuery
.output(
z.array(
z.object({
id: z.string(),
body: z.string(),
createdAt: z.date(),
})
)
)
.query(async ({ ctx }) => {
const rows = await ctx.db.query('messages').order('desc').take(10);
return rows.map((row) => ({
id: row._id,
body: row.body,
createdAt: new Date(row._creationTime),
}));
});
export const create = publicMutation
.input(z.object({ body: z.string().trim().min(1).max(120) }))
.output(z.string())
.mutation(async ({ ctx, input }) =>
await ctx.db.insert('messages', { body: input.body })
);The pattern is simple:
publicQueryreads datapublicMutationwrites datazoddefines the contract- the return value is shaped for the client immediately
The starter demo uses direct ctx.db calls because they are easy to read. The
rest of the docs build on top of ctx.orm once you need richer schemas,
relations, and typed query helpers.
cRPC builder
The builders come from the scaffolded cRPC file:
import { initCRPC } from '../functions/generated/server';
const c = initCRPC.create();
export const publicQuery = c.query;
export const publicAction = c.action;
export const publicMutation = c.mutation;
export const privateQuery = c.query.internal();
export const privateMutation = c.mutation.internal();
export const privateAction = c.action.internal();That file is part of the baseline. You do not need to hand-roll cRPC setup in
the quickstart. init already gave you the builders the starter app needs.
4. Read The Starter Client
The client side is just as small.
React cRPC context
We'll start with the generated client context:
import { api } from '@convex/api';
import { createCRPCContext } from 'kitcn/react';
export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
api,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});@convex/api is the generated metadata surface. createCRPCContext(...)
turns that into typed TanStack Query helpers for your React app.
Demo page
Now let's look at the starter page you just opened:
'use client';
import { useMutation, useQuery } from '@tanstack/react-query';
import { type FormEvent, useState } from 'react';
import { Button } from '@/components/ui/button';
import { useCRPC } from '@/lib/convex/crpc';
export default function ConvexMessagesPage() {
const crpc = useCRPC();
const [draft, setDraft] = useState('');
const messagesQuery = useQuery(crpc.messages.list.queryOptions());
const createMessage = useMutation(crpc.messages.create.mutationOptions());
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const body = draft.trim();
if (!body) return;
try {
await createMessage.mutateAsync({ body });
setDraft('');
} catch {}
}
return (
<main>
<form onSubmit={handleSubmit}>
<input
onChange={(event) => setDraft(event.target.value)}
value={draft}
/>
<Button disabled={createMessage.isPending} type="submit">
{createMessage.isPending ? 'Saving...' : 'Add message'}
</Button>
</form>
{messagesQuery.isPending ? (
<p>Loading messages...</p>
) : messagesQuery.isError ? (
<div>Backend not ready. Start kitcn dev and refresh.</div>
) : (
<ul>
{messagesQuery.data.map((message) => (
<li key={message.id}>{message.body}</li>
))}
</ul>
)}
</main>
);
}This is the starter loop you will reuse everywhere:
useCRPC()gives you typed procedure handlesuseQuery(...)reads data from a query procedureuseMutation(...)calls a mutation procedure- the UI handles loading, error, and success states in the same file
5. What To Build Next
You now have a running kitcn app, a local deployment, generated API types, and one live page wired end to end. That's the real starting point.
From here, pick the next seam you actually need: