kitcn

Quickstart

Scaffold a Next.js app with kitcn, bootstrap a local backend, and understand the starter messages demo.

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

mkdir my-app
cd my-app
npx kitcn@latest init -t next --yes
pnpm dlx kitcn@latest init -t next --yes
yarn dlx kitcn@latest init -t next --yes
bunx --bun kitcn@latest init -t next --yes

init -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.

Fresh TanStack Start, Vite, and Expo starters are also available through kitcn init -t start --yes, kitcn init -t vite --yes, and kitcn init -t expo --yes.

If you want the native-first path, use /docs/expo instead of forcing the Next quickstart into a mobile app.

Here are the key files the quickstart uses:

FilePurpose
convex/functions/schema.tsStarter schema
convex/functions/messages.tsStarter query and mutation
convex/lib/crpc.tsServer-side cRPC builders
lib/convex/crpc.tsxReact cRPC context
components/providers.tsxApp-level providers
app/convex/page.tsxStarter demo page

If you already have a supported app shell, use npx kitcn@latest init --yes instead. That adoption path is documented in /docs/cli/registry.

2. Run The App

Start the backend and open the Next.js app in a second terminal.

terminal 1
npx kitcn dev
terminal 2
npm run dev

Open 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

Schema

The starter schema is intentionally tiny.

convex/functions/schema.ts
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

The starter procedures:

convex/functions/messages.ts
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:

  • publicQuery reads data
  • publicMutation writes data
  • zod defines 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:

convex/lib/crpc.ts
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

The generated client context:

lib/convex/crpc.tsx
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

The starter page:

app/convex/page.tsx
'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 handles
  • useQuery(...) reads data from a query procedure
  • useMutation(...) calls a mutation procedure
  • the UI handles loading, error, and success states in the same file

5. What To Build Next

From here, pick the next seam you actually need:

On this page