fullstack-vite-convex
Full-Stack Web Development — Convex + Vite React
Build production-quality Convex + Vite React applications with test-driven development and strict TypeScript. Handle the entire stack end-to-end: scaffolding, tests, database, backend, frontend, styling, starting servers, verifying the build, running tests, and delivering a running app.
Core Principles
1. Autonomy Is Non-Negotiable
- NEVER tell the user to run commands. YOU run them.
- NEVER say "you can now run..." or "please execute...". Just do it.
- Scaffold the project, install deps, write all code, start all servers, seed data, run tests, verify the build — all yourself.
- The user should receive a working, running, tested application with a URL they can open.
- If something fails, fix it yourself. Don't report errors without attempting resolution.
2. TDD By Default
- Write tests BEFORE implementation. Always.
- Backend: write Convex function tests before writing the functions.
- Frontend: write component tests before writing the components.
- Every feature gets a test. No exceptions.
- Tests must pass before moving to the next phase. Run them yourself and fix failures.
3. Strict TypeScript — Zero Tolerance
- All code uses strict TypeScript. No
any. Noas unknown as Xhacks. No@ts-ignore. - Enable all strict flags in
tsconfig.json—strict: true,noUncheckedIndexedAccess: true,noImplicitReturns: true,noFallthroughCasesInSwitch: true,exactOptionalPropertyTypes: true. - Every function has explicit return types. Every variable has a type or is inferable.
- Use
Id<"tableName">for Convex IDs, neverstring. - Use discriminated unions with
as constfor status/kind fields. npx tsc --noEmitmust produce 0 errors before you deliver. Run it and fix every error.
Documentation Lookup
Always use Context7 MCP tools (resolve-library-id then query-docs) when you need library, API, or framework documentation. Do NOT ask the user. Proactively use Context7 whenever the task involves a library, framework, or API you are not fully confident about. This includes Convex, React, Vite, Tailwind, Vitest, any npm package, or third-party API.
Workflow
Phase 1: Scaffold & Setup (Local by Default)
Scaffold the project yourself — no Convex account or cloud needed:
npm create convex@latest -- -t react-vite my-app && cd my-app && npm install
Install ALL deps in one shot — testing, styling, utilities:
npm install lucide-react && npm install -D tailwindcss @tailwindcss/vite vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @types/node
Project structure:
my-app/
convex/ # Backend
_generated/ # Auto-generated (never edit)
schema.ts
tsconfig.json
src/
components/ # React components
hooks/ # Custom hooks
lib/ # Utilities, types, constants
__tests__/ # Frontend tests
App.tsx
main.tsx
tests/ # Backend/integration tests
package.json
tsconfig.json
vite.config.ts
vitest.config.ts
Phase 2: Configure Strict TypeScript
Set up tsconfig.json with maximum strictness:
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src/**/*", "tests/**/*", "vite.config.ts", "vitest.config.ts"],
"exclude": ["convex"]
}
Set up vitest.config.ts:
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test-setup.ts"],
include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.ts"],
coverage: {
provider: "v8",
reporter: ["text", "lcov"],
},
},
});
Create src/test-setup.ts:
import "@testing-library/jest-dom/vitest";
Add test scripts to package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"lint": "tsc --noEmit && vitest run"
}
}
Phase 3: Schema & Types
Define schema in convex/schema.ts and export shared types in src/lib/types.ts.
Define all data types, constants, and enums upfront. Use discriminated unions for status fields:
// src/lib/types.ts
export const BOOKING_STATUS = {
pending: "pending",
confirmed: "confirmed",
cancelled: "cancelled",
} as const;
export type BookingStatus = (typeof BOOKING_STATUS)[keyof typeof BOOKING_STATUS];
Phase 4: Write Tests First (TDD)
Backend tests — test Convex function logic (validators, edge cases):
// tests/services.test.ts
import { describe, it, expect } from "vitest";
describe("services", () => {
it("should validate service has required fields", () => {
const service = {
name: "Consultation",
description: "1-on-1 session",
duration: 60,
price: 150,
category: "consulting",
available: true,
icon: "phone",
};
expect(service.name).toBeDefined();
expect(service.price).toBeGreaterThan(0);
expect(service.duration).toBeGreaterThan(0);
});
it("should reject invalid price", () => {
expect(() => {
if (-1 <= 0) throw new Error("Price must be positive");
}).toThrow("Price must be positive");
});
});
Frontend component tests — test rendering, user interactions:
// src/__tests__/ServiceCard.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { ServiceCard } from "../components/ServiceCard";
describe("ServiceCard", () => {
const mockService = {
_id: "test-id" as any,
_creationTime: Date.now(),
name: "Consultation",
description: "1-on-1 session",
duration: 60,
price: 150,
category: "consulting",
available: true,
icon: "phone",
};
it("renders service name and price", () => {
render(<ServiceCard service={mockService} onBook={() => {}} />);
expect(screen.getByText("Consultation")).toBeInTheDocument();
expect(screen.getByText(/\$150/)).toBeInTheDocument();
});
it("shows unavailable state when not available", () => {
render(<ServiceCard service={{ ...mockService, available: false }} onBook={() => {}} />);
expect(screen.getByText(/unavailable/i)).toBeInTheDocument();
});
});
Run tests — they should fail (red phase):
npx vitest run
Phase 5: Implement Code (Green Phase)
Now write the implementation to make tests pass:
- Backend functions — queries, mutations, actions, seed data in
convex/ - Frontend components — each in its own file with typed props interfaces
- Hooks — custom hooks for shared logic
- Pages — route-level components composing smaller pieces
Run tests again — they must pass (green phase):
npx vitest run
Fix any failures before proceeding.
Phase 6: Refactor
With passing tests as a safety net, refactor:
- Extract shared logic into hooks/utilities
- Remove duplication
- Improve component composition
- Tighten types
Run tests after every refactor to ensure nothing broke.
Phase 7: Start Servers & Verify
Start the local Convex backend:
npx convex dev --local &
Start the Vite dev server:
npm run dev &
Seed data if needed:
npx convex run --local myFile:seedFunction
Phase 8: Final Verification
Run the full verification pipeline — ALL must pass:
npx tsc --noEmit && npx vitest run && npm run build
This checks:
- TypeScript — 0 type errors (strict mode)
- Tests — all tests pass
- Build — Vite compiles cleanly
Fix any failures yourself. Do not deliver until all 3 pass with 0 errors.
Phase 9: Deliver
Report to the user:
- The running app URL (e.g.
http://localhost:5173) - What was built — features, pages, backend functions
- Test results — X tests passing, 0 failures
- Build status — 0 TypeScript errors, 0 build errors
- Mention: "Running locally with a local Convex backend. When you want to deploy to the cloud, run
npx convex loginthennpx convex deploy."
Schema Design
Always define the schema first in convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
}).index("by_email", ["email"]),
messages: defineTable({
authorId: v.id("users"),
content: v.string(),
channelId: v.id("channels"),
}).index("by_channel", ["channelId"]),
});
Rules:
- Always include all index fields in the index name (e.g.
by_field1_and_field2) - System fields
_idand_creationTimeare auto-added — never define them - Field names must not start with
$or_
Backend Functions
Write functions in convex/ using the NEW function syntax. Every function MUST have args and returns validators.
Public functions (exposed to clients):
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({
_id: v.id("messages"),
_creationTime: v.number(),
content: v.string(),
authorId: v.id("users"),
})),
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(50);
},
});
export const send = mutation({
args: { channelId: v.id("channels"), authorId: v.id("users"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("messages", args);
return null;
},
});
Internal functions (only callable from other Convex functions):
import { internalAction, internalMutation, internalQuery } from "./_generated/server";
Actions (for external API calls, use "use node"; for Node.js modules):
"use node";
import { internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
export const callExternalAPI = internalAction({
args: { prompt: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const response = await fetch("https://api.example.com/...");
const data = await response.json();
await ctx.runMutation(internal.myFile.saveResult, { data });
return null;
},
});
Frontend
Use convex/react hooks for real-time data. Always type props with interfaces:
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import type { Id } from "../convex/_generated/dataModel";
interface ChatProps {
readonly channelId: Id<"channels">;
readonly authorId: Id<"users">;
}
function Chat({ channelId, authorId }: ChatProps): React.ReactElement {
const messages = useQuery(api.messages.list, { channelId });
const sendMessage = useMutation(api.messages.send);
const handleSend = async (content: string): Promise<void> => {
await sendMessage({ channelId, authorId, content });
};
if (messages === undefined) {
return <ChatSkeleton />;
}
return (
<div>
{messages.map((msg) => (
<div key={msg._id}>{msg.content}</div>
))}
</div>
);
}
The main.tsx must wrap the app with ConvexProvider:
import React from "react";
import ReactDOM from "react-dom/client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import App from "./App";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("Root element not found");
ReactDOM.createRoot(rootEl).render(
<React.StrictMode>
<ConvexProvider client={convex}>
<App />
</ConvexProvider>
</React.StrictMode>,
);
Styling
- Use Tailwind CSS (install if not present:
npm install -D tailwindcss @tailwindcss/vite) - Responsive design — mobile hamburger menu, responsive grids, touch-friendly targets
- Clean component structure — one component per file, typed props interface
- Use
lucide-reactfor icons - Skeleton loading states for async data
- Toast notifications for user actions (success/error/info)
Convex Rules (Critical)
Functions
- ALWAYS use the new function syntax with
argsandreturnsvalidators - If a function returns nothing, use
returns: v.null()andreturn null v.bigint()is DEPRECATED — usev.int64()instead- Use
v.record(keys, values)for dynamic key objects —v.map()andv.set()are NOT supported - Use
api.file.functionNamefor public function references,internal.file.functionNamefor internal - You CANNOT register a function through the
apiorinternalobjects
Queries
- Do NOT use
.filter()— define an index and use.withIndex()instead - Convex queries do NOT support
.delete()— collect results and callctx.db.delete(row._id)on each - Use
.unique()for single document queries - Default order is ascending
_creationTime. Use.order("desc")for reverse.
Mutations
ctx.db.patch(id, fields)— shallow merge updatectx.db.replace(id, fullDocument)— full replace- Both throw if document doesn't exist
Actions
- Add
"use node";at top of files using Node.js built-ins - Actions do NOT have
ctx.db— they cannot access the database directly - Use
ctx.runQuery/ctx.runMutationto interact with DB from actions - Minimize action-to-query/mutation calls (each is a separate transaction = race condition risk)
Scheduling
- Use
ctx.scheduler.runAfter(delayMs, functionRef, args)for delayed execution - Use
ctx.scheduler.runAt(timestamp, functionRef, args)for specific time execution - Crons: use
crons.interval()orcrons.cron()only — NOTcrons.hourly/daily/weekly
File Storage
ctx.storage.getUrl(storageId)returns a signed URL (or null)- Query
_storagesystem table for metadata:ctx.db.system.get(storageId)
HTTP Endpoints
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/webhook",
method: "POST",
handler: httpAction(async (ctx, req) => {
const body = await req.json();
return new Response(null, { status: 200 });
}),
});
export default http;
TypeScript
- Use
Id<"tableName">from./_generated/dataModelfor typed IDs — NEVERstring - Use
Doc<"tableName">from./_generated/dataModelfor full document types - Use
as constfor string literals in discriminated unions - Add
@types/nodeto package.json when using Node.js modules - All functions have explicit return types
- All component props use
readonlyinterface fields - Never use
any— useunknownand narrow with type guards
Deployment (Cloud — Only When User Asks)
Local dev is the default. Only handle cloud deployment when the user explicitly asks.
- Login —
npx convex login(the ONE step requiring user interaction — opens browser). Tell the user. - Link —
npx convex dev(without--local) - Deploy —
npx convex deploy
The .env.local updates automatically. No code changes needed.
General Rules
- Match existing project conventions if working in an existing codebase
- Verify the dev server runs and check for errors — fix them yourself
- Install dependencies via npm as needed
- The verification pipeline
npx tsc --noEmit && npx vitest run && npm run buildmust produce 0 errors before delivering - Default to local Convex — never prompt for cloud login unless the user asks to deploy
- You deliver running, tested apps — not instructions.
More from kortix-ai/kortix-registry
openalex-paper-search
Academic paper search powered by OpenAlex -- the free, open catalog of 240M+ scholarly works. Use when the user needs to find academic papers, research articles, literature for a topic, citation data, author publications, or any scholarly source. Triggers on: 'find papers on', 'academic research about', 'what studies exist', 'literature review', 'find citations', 'scholarly articles about', 'who published on', 'papers by [author]', 'highly cited papers on', any request for peer-reviewed or academic sources. Also use during deep research when you need to ground findings in academic literature. Do NOT use for general web searches -- use web-search for that.
202legal-writer
Legal document drafting -- contracts, memos, briefs, complaints, demand letters, opinions, discovery, settlements, ToS, privacy policies. Full pipeline: document structure, per-section writing, Bluebook citation, case law lookup (CourtListener API), regulation lookup (eCFR API), DOCX output, and TDD-style verification (defined terms, cross-references, placeholders, boilerplate, citation format). Triggers on: 'draft a contract', 'write a legal memo', 'create an NDA', 'write a brief', 'legal document about', 'draft a complaint', 'terms of service', 'privacy policy', 'demand letter', 'settlement agreement', 'legal opinion', 'discovery requests', any request to produce a legal or law-related document.
58paper-creator
Scientific paper writing in LaTeX -- full pipeline from structure to compiled PDF. TDD-driven: every section is compiled and verified before moving to the next. Covers project scaffolding, citation management (OpenAlex to BibTeX), per-section academic writing with self-reflection, figure/table inclusion, LaTeX compilation, and comprehensive verification. Triggers on: 'write a paper', 'create a paper', 'academic paper about', 'scientific paper', 'LaTeX paper', 'write up results as a paper', 'draft a paper on', 'research paper about', any request to produce a formal academic/scientific paper in LaTeX. Assumes research findings, data, and/or figures already exist or will be provided -- this skill handles the WRITING, not the experimentation.
11deep-research
Deep research agent skill. Use when the user needs thorough, scientific, truth-seeking research on any topic -- investigating claims, finding primary sources, synthesizing evidence, producing cited reports. Triggers on: 'research this', 'investigate', 'deep dive', 'find sources', 'what does the evidence say', 'literature review', 'fact check', 'analyze the research on', any request requiring multi-source investigation with citations.
10memory-context-management
Memory, context, and persistent knowledge management for the Kortix agent. Covers: kortix-sys-oc-plugin (observations, LTM consolidation, mem_search, mem_save, session_list, session_get), filesystem persistence rules, using .MD files for plans/notes/project state, how filesystem writes feed the memory pipeline, and best practices for ensuring nothing important is ever lost. Load this skill when you need to: understand how your memory works, decide where to persist information, write plans or notes, manage project context across sessions, or optimize your context window usage.
8domain-research
Free domain research and availability checking. No API keys or credentials required. Uses RDAP (1195+ TLDs) with whois CLI fallback for universal coverage. Checks if domains are available, searches keywords across TLDs, performs WHOIS/RDAP lookups, checks expiry dates, and finds nameservers. Use when the agent needs to: check if a domain is available, search for domains, find who owns a domain, check domain expiration, get nameservers, bulk check domains, or do any domain research. Triggers on: 'check domain', 'is domain available', 'search domains', 'domain availability', 'who owns this domain', 'whois', 'domain expiry', 'when does domain expire', 'nameservers for', 'domain research', 'find domains for', 'domain ideas', 'bulk domain check'.
7