This document provides guidelines for AI coding agents working in this repository.
Kivo is a full-stack invoicing application built as an npm workspaces monorepo:
- apps/api - Cloudflare Workers backend using Hono framework
- apps/web - React frontend with Vite, TanStack Router, and shadcn/ui
- packages/shared - Shared types, schemas, and utilities
npm run dev # Start both API and web dev servers concurrently
npm run dev:api # Start API dev server only
npm run dev:web # Start web dev server only
npm run build # Build all packages (shared → api → web)
npm run test # Run tests across all workspaces
npm run lint # ESLint for .ts and .tsx files
npm run typecheck # TypeScript type checkingnpm run dev -w apps/api # Start Wrangler dev server
npm run build -w apps/api # Build for Cloudflare Workers
npm run deploy -w apps/api # Deploy to Cloudflare
npm run test -w apps/api # Run API tests
npm run db:migrate -w apps/api # Run D1 migrations (remote)
npm run db:migrate:local -w apps/api # Run D1 migrations (local)npm run dev -w apps/web # Start Vite dev server
npm run build -w apps/web # Build for production
npm run lint -w apps/web # Lint web app# Run a specific test file
npm run test -w apps/api -- src/durable-objects/reminder-scheduler.test.ts
npm run test -w packages/shared -- src/utils.test.ts
# Run tests matching a pattern
npm run test -w packages/shared -- --grep "calculateInvoiceTotals"
# Run tests in watch mode
npm run test -w packages/shared -- --watch- Target: ES2022
- Module: ESNext with bundler resolution
- Strict mode enabled
- No unused locals/parameters
- No fallthrough in switch statements
Order imports as follows:
- External packages (hono, react, zod, etc.)
- Type imports from external packages
- Internal package imports (@kivo/shared)
- Type imports from internal packages
- Relative imports (local files)
- Type-only relative imports
// Example - API route
import { Hono } from 'hono';
import type { Env, Variables } from '../types';
import { createClientSchema } from '@kivo/shared';
import type { Client } from '@kivo/shared';
import { ValidationError, NotFoundError } from '../utils/errors';// Example - React component
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';- Files: kebab-case (
error-handler.ts,invoice-form.tsx) - Components: PascalCase (
Button,InvoiceForm) - Functions/Variables: camelCase (
handleResponse,userId) - Constants: SCREAMING_SNAKE_CASE (
INVOICE_STATUSES,API_BASE) - Types/Interfaces: PascalCase (
Client,InvoiceStatus) - Zod Schemas: camelCase with
Schemasuffix (clientSchema,createInvoiceSchema) - Database fields: snake_case (
user_id,created_at)
Use Zod schemas as the single source of truth. Infer TypeScript types from schemas:
// In packages/shared/src/schemas.ts
export const clientSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(200),
email: z.string().email(),
// ...
});
// In packages/shared/src/types.ts
export type Client = z.infer<typeof clientSchema>;Use custom error classes extending AppError:
import { ValidationError, NotFoundError, AuthorizationError } from '../utils/errors';
// Validation errors
if (!result.success) {
throw new ValidationError('Invalid input', result.error.flatten());
}
// Not found errors
if (!client) {
throw new NotFoundError('Client');
}Available error classes:
ValidationError(400)AuthenticationError(401)AuthorizationError(403)NotFoundError(404)ConflictError(409)RateLimitError(429)ExternalServiceError(502)
Use the ApiError class and handle in components:
try {
await clientsApi.create(data);
} catch (error) {
if (error instanceof ApiError) {
toast({ title: 'Error', description: error.message, variant: 'destructive' });
}
}All API responses follow this structure:
// Success response
{ data: T, requestId: string }
// Paginated response
{ data: T[], pagination: { page, limit, total, total_pages }, requestId: string }
// Error response
{ error: { code: string, message: string, details?: unknown }, requestId: string }Use JSDoc comments and consistent structure:
/**
* Create a new client
*/
clients.post('/', async (c) => {
const userId = c.get('userId')!;
const requestId = c.get('requestId');
const body = await c.req.json();
const result = createClientSchema.safeParse(body);
if (!result.success) {
throw new ValidationError('Invalid input', result.error.flatten());
}
// ... business logic
return c.json({ data: client, requestId }, 201);
});Use forwardRef for UI components, CVA for variants:
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';Use Vitest with describe/it/expect pattern:
import { describe, it, expect } from 'vitest';
describe('calculateInvoiceTotals', () => {
it('should calculate subtotal correctly', () => {
const items = [{ quantity: 2, unit_price: 100 }];
const result = calculateInvoiceTotals(items);
expect(result.subtotal).toBe(200);
});
});Use parameterized queries with D1:
const result = await c.env.DB.prepare(
'SELECT * FROM clients WHERE user_id = ? AND archived = ?'
).bind(userId, showArchived ? 1 : 0).all<Client>();- Frontend:
@/*maps to./src/* - Use
@kivo/sharedfor shared package imports
- Use
cn()utility for conditional classes - Follow shadcn/ui patterns for component styling
- CSS variables for theming (defined in globals.css)
- Authentication: Magic link email authentication with JWT tokens
- Database: Cloudflare D1 (SQLite)
- Storage: Cloudflare R2 for PDFs and logos
- Background Jobs: Durable Objects for reminder scheduling
- Payments: Stripe integration
- State Management: TanStack Query for server state
- Forms: react-hook-form with Zod validation