A CLAUDE.md is just a markdown file at the root of your repo. Copy the content below into your own project's CLAUDE.md to give your agent the same context.
npx versuz@latest install usekaneo-kaneo --kind=claude-mdcurl -o CLAUDE.md https://raw.githubusercontent.com/usekaneo/kaneo/HEAD/CLAUDE.md# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Kaneo is a self-hosted project management platform built with simplicity and performance as core principles. The codebase is organized as a **pnpm monorepo** with TurboRepo.
**Key Philosophy**: Features exist to solve real problems, not to impress. Avoid over-engineering - keep solutions simple and focused. Don't add features, refactoring, or improvements beyond what was asked.
## Development Commands
### Getting Started
```bash
# Install dependencies (uses pnpm)
pnpm install
# Start all development servers (API + web)
pnpm dev
# Lint and auto-fix code (Biome)
pnpm lint
# Build all packages
pnpm build
```
### API-Specific Commands
```bash
# Run API in development mode
pnpm --filter @kaneo/api dev
# Build API
pnpm --filter @kaneo/api build
# Generate database migrations (after schema changes)
pnpm --filter @kaneo/api db:generate
# Run database migrations (auto-runs on API startup)
pnpm --filter @kaneo/api db:migrate
# Open Drizzle Studio (database GUI)
pnpm --filter @kaneo/api db:studio
# Lint API code
pnpm --filter @kaneo/api lint
```
### Web-Specific Commands
```bash
# Run web app in development mode
pnpm --filter @kaneo/web dev
# Build web app for production
pnpm --filter @kaneo/web build
# Preview production build
pnpm --filter @kaneo/web preview
# Lint web code
pnpm --filter @kaneo/web lint
```
## Architecture Overview
### Monorepo Structure
```
kaneo/
├── apps/
│ ├── api/ # Backend API (Hono/Node.js/PostgreSQL)
│ ├── web/ # Frontend app (React/Vite/TanStack)
│ └── docs/ # Documentation site (Next.js)
├── packages/
│ ├── email/ # Email utilities
│ ├── libs/ # Shared libraries
│ └── typescript-config/ # TypeScript configurations
└── charts/ # Kubernetes Helm charts
```
### Technology Stack
**Backend (API)**
- Framework: Hono (lightweight web framework)
- Database: PostgreSQL with Drizzle ORM
- Authentication: Better Auth
- Validation: Valibot (Zod is also present, used by Better Auth and some schemas)
- API Documentation: OpenAPI (hono-openapi)
- IDs: CUID2 (via @paralleldrive/cuid2)
**Frontend (Web)**
- Framework: React 19+
- Routing: TanStack Router (file-based)
- Data Fetching: TanStack Query (React Query)
- Build Tool: Vite
- Styling: Tailwind CSS v4
- State Management: Zustand
- UI Components: Radix UI primitives
### Key Architectural Patterns
**Backend API Structure**
- Routes organized by feature in `apps/api/src/{feature}/`
- Controller pattern: business logic extracted to `{feature}/controllers/`
- All routes use OpenAPI decorators (`describeRoute`)
- All inputs validated with Valibot schemas
- Migrations auto-run on API startup
**Frontend Structure**
- File-based routing in `apps/web/src/routes/`
- Query hooks in `apps/web/src/hooks/queries/`
- Mutation hooks in `apps/web/src/hooks/mutations/`
- API fetchers in `apps/web/src/fetchers/{feature}/`
- Components in `apps/web/src/components/`
**Database Schema Conventions**
- All tables use CUID2 for primary keys (`createId()`)
- Every table has `createdAt` and `updatedAt` timestamps
- Foreign keys always specify cascade behavior (`onDelete`, `onUpdate`)
- Indexes on frequently queried columns (especially foreign keys)
- Schema defined in `apps/api/src/database/schema.ts`
- Relations defined in `apps/api/src/database/relations.ts`
**Authentication Flow**
- Better Auth handles authentication
- User context available in Hono via `c.get("userId")`, `c.get("user")`, `c.get("session")`
- API keys supported via Bearer token
- Frontend uses Better Auth client from `@/lib/auth-client`
**Event System**
- Events published for activity tracking
- Use `publishEvent()` from `apps/api/src/events/`
- Events tracked for features like status changes, assignments, etc.
## Code Style
### Formatting (Biome)
- **Indentation**: Spaces for JavaScript/TypeScript/TSX (tabs for other file types)
- **Quotes**: Double quotes
- **Semicolons**: Required
- **Ignored files**: CSS and `package.json` files are excluded from Biome linting/formatting
- Run `pnpm lint` to auto-fix
### TypeScript Conventions
- Prefer `type` over `interface` (only use interface when extending/merging)
- Prefer type inference when obvious
- File naming: PascalCase for components, kebab-case for utilities/hooks
- Hooks use `use` prefix: `use-task.ts`
### Import Organization
1. External packages
2. Internal packages (`@/` aliases)
3. Relative imports
Biome auto-organizes imports.
### Git Commits
Use Conventional Commits:
- `feat:` - New features
- `fix:` - Bug fixes
- `docs:` - Documentation
- `refactor:` - Code refactoring
- `chore:` - Maintenance tasks
Husky enforces commit message format via commitlint.
### Pre-commit Hooks
The pre-commit hook (`.husky/pre-commit`) runs two checks:
1. `biome ci .` — linting and formatting validation
2. `pnpm run build` — full monorepo build
Commits will be slow due to the build step. Ensure code compiles before committing.
## Environment Configuration
**Single `.env` file** in project root shared by all apps.
Required variables:
- `KANEO_CLIENT_URL` - Web app URL (e.g., http://localhost:5173)
- `KANEO_API_URL` - API URL (e.g., http://localhost:1337)
- `AUTH_SECRET` - JWT secret (min 32 chars)
- `DATABASE_URL` - PostgreSQL connection string
- `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`
Optional:
- `CORS_ORIGINS` - Comma-separated allowed origins (empty = allow all in dev)
- `VITE_API_URL` - API URL for web dev (defaults to http://localhost:1337)
- `REDIS_URL` - Redis connection string for multi-instance WebSocket broadcasts via Pub/Sub (omit for single-instance in-memory mode)
- SSO providers (GitHub, Google, Discord, Custom OAuth/OIDC)
- SMTP configuration
See `ENVIRONMENT_SETUP.md` for detailed configuration and troubleshooting.
## Development Workflow
### When Making Changes
1. **Read before modifying**: Never propose changes to code you haven't read
2. **Use existing patterns**: Follow the established controller/fetcher/hook patterns
3. **Avoid over-engineering**: Don't add features beyond what's requested
4. **Type safety**: Let TypeScript guide you - all APIs are fully typed
5. **Validate inputs**: Always use Valibot schemas for API inputs
6. **Error handling**: Backend uses HTTPException, frontend uses toast notifications
### Database Changes
1. Modify schema in `apps/api/src/database/schema.ts`
2. Generate migration: `pnpm --filter @kaneo/api db:generate`
3. Migration auto-runs on next API startup
4. Always use CUID2 for IDs, include timestamps, specify cascade behavior
### Adding API Endpoints
1. Create controller in `apps/api/src/{feature}/controllers/`
2. Add route in `apps/api/src/{feature}/index.ts`
3. Use `describeRoute` for OpenAPI docs
4. Use `validator` with Valibot schema
5. Keep route handler thin - business logic in controller
### Adding Frontend Features
1. Create fetcher in `apps/web/src/fetchers/{feature}/`
2. Create query/mutation hook in `apps/web/src/hooks/`
3. Use TanStack Query for caching
4. Handle loading/error states properly
5. Use toast notifications (sonner) for user feedback
## Important Notes
- **Package Manager**: This project uses **pnpm** (pinned to `10.32.1` via `packageManager` field), not npm or yarn. Requires Node `>=18`
- **Migrations**: Auto-run on API startup, stored in `apps/api/drizzle/`
- **Development Ports**: API runs on 1337, web runs on 5173
- **Hot Reload**: Both API and web have watch mode via `pnpm dev`
- **CORS**: Configured in API index.ts, controlled by `CORS_ORIGINS` env var
- **Testing**: Run `pnpm test` at the repo root (Turbo runs `test` in packages that define it: API unit tests, web unit/component tests, shared packages). API integration tests: `pnpm test:integration` (requires PostgreSQL; env is set in `tests/api-integration/setup.ts`; CI uses `.github/workflows/ci.yml`). Vitest configs: `apps/api/vitest.config.ts` (unit), `apps/api/vitest.integration.config.ts` (integration), `apps/web/vitest.config.ts` (web). Integration tests live under `tests/api-integration/`; API unit tests under `tests/api/`.
- **Security**: Never commit secrets, always validate inputs, sanitize outputs
## Common Patterns
### Backend Route Example
```typescript
// apps/api/src/{feature}/index.ts
import { Hono } from "hono";
import { describeRoute, validator } from "hono-openapi";
import * as v from "valibot";
import getItem from "./controllers/get-item";
const feature = new Hono<{ Variables: { userId: string } }>()
.get("/:id",
describeRoute({
operationId: "getItem",
tags: ["Feature"],
description: "Get item by ID"
}),
validator("param", v.object({ id: v.string() })),
async (c) => {
const { id } = c.req.valid("param");
const item = await getItem(id);
return c.json(item);
}
);
```
### Frontend Query Hook Example
```typescript
// apps/web/src/hooks/queries/{feature}/use-item.ts
import { useQuery } from "@tanstack/react-query";
import { getItem } from "@/fetchers/{feature}/get-item";
export function useItem(itemId: string) {
return useQuery({
queryKey: ["item", itemId],
queryFn: () => getItem(itemId),
});
}
```
### Database Schema Example
```typescript
// apps/api/src/database/schema.ts
export const exampleTable = pgTable("example", {
id: text("id").$defaultFn(() => createId()).primaryKey(),
projectId: text("project_id")
.notNull()
.references(() => projectTable.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
title: text("title").notNull(),
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { mode: "date" })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
}, (table) => [
index("example_projectId_idx").on(table.projectId),
]);
```