A modern full-stack TypeScript monorepo combining Next.js for the frontend, Convex for the real-time backend database, and Cloudflare Workers for edge compute.
- Frontend: Next.js 16, React 19, TailwindCSS v4
- Database: Convex (real-time, serverless)
- Edge API: Cloudflare Workers
- UI: shadcn/ui components
- Testing: Vitest, Playwright
- Tooling: pnpm workspaces, TypeScript, ESLint, Prettier
- Node.js 20+
- pnpm 9.15+
- A Convex account
# Install dependencies
pnpm install
# Start the Convex backend (creates a dev deployment on first run)
pnpm dev:convex
# In another terminal, start the web app
pnpm dev:webThe web app runs at http://localhost:3000 and the Cloudflare Worker at http://localhost:8787.
After running pnpm dev:convex for the first time, Convex creates a .env.local in packages/backend/ with your deployment URL. Copy the URL to apps/web/.env.local:
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud├── apps/
│ ├── web/ # Next.js frontend
│ └── worker/ # Cloudflare Workers edge API
├── packages/
│ ├── backend/ # Convex database & functions
│ │ └── convex/ # Schema and server functions
│ └── ui/ # Shared shadcn/ui components
└── e2e/ # Playwright end-to-end tests
| Command | Description |
|---|---|
pnpm dev |
Start all apps in development |
pnpm dev:convex |
Start Convex backend |
pnpm dev:web |
Start Next.js only |
pnpm dev:worker |
Start Cloudflare Worker only |
pnpm test |
Run unit tests |
pnpm test:e2e |
Run Playwright E2E tests |
pnpm lint |
Run ESLint |
pnpm typecheck |
Run TypeScript checks |
pnpm build |
Build all apps |
Edit packages/backend/convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tasks: defineTable({
text: v.string(),
isCompleted: v.boolean(),
}),
});Create queries and mutations in packages/backend/convex/:
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
});
export const create = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert("tasks", { text: args.text, isCompleted: false });
},
});import { api } from "@workspace/backend/convex/_generated/api";
import { useQuery, useMutation } from "convex/react";
export function TaskList() {
const tasks = useQuery(api.tasks.list);
const createTask = useMutation(api.tasks.create);
return (
<div>
{tasks?.map((task) => (
<div key={task._id}>{task.text}</div>
))}
<button onClick={() => createTask({ text: "New task" })}>Add</button>
</div>
);
}Add shadcn components from the web app directory:
cd apps/web
npx shadcn@latest add button --yesImport in your code:
import { Button } from "@workspace/ui/components/ui/button";This project includes recommended extensions and sample settings:
.vscode/extensions.json— Prompts to install recommended extensions (ESLint, Prettier, Tailwind CSS IntelliSense, dotenv).vscode/settings.sample.json— Optional workspace settings template
To use the recommended settings, copy the sample file:
cp .vscode/settings.sample.json .vscode/settings.jsonYour personal settings.json is gitignored, so you can customize it without affecting others.
pnpm --filter @workspace/backend deploypnpm --filter @workspace/worker deployDeploy to Vercel, Netlify, or any Node.js hosting platform. Set NEXT_PUBLIC_CONVEX_URL to your production Convex deployment URL.