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#
# 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#
# 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#
# 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
createdAtandupdatedAttimestamps - 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()fromapps/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.jsonfiles are excluded from Biome linting/formatting - Run
pnpm lintto auto-fix
TypeScript Conventions#
- Prefer
typeoverinterface(only use interface when extending/merging) - Prefer type inference when obvious
- File naming: PascalCase for components, kebab-case for utilities/hooks
- Hooks use
useprefix:use-task.ts
Import Organization#
- External packages
- Internal packages (
@/aliases) - Relative imports Biome auto-organizes imports.
Git Commits#
Use Conventional Commits:
feat:- New featuresfix:- Bug fixesdocs:- Documentationrefactor:- Code refactoringchore:- Maintenance tasks
Husky enforces commit message format via commitlint.
Pre-commit Hooks#
The pre-commit hook (.husky/pre-commit) runs two checks:
biome ci .— linting and formatting validationpnpm 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 stringPOSTGRES_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)- SSO providers (GitHub, Google, Discord, Custom OAuth/OIDC)
- SMTP configuration
See ENVIRONMENT_SETUP.md for detailed configuration and troubleshooting.
Development Workflow#
When Making Changes#
- Read before modifying: Never propose changes to code you haven't read
- Use existing patterns: Follow the established controller/fetcher/hook patterns
- Avoid over-engineering: Don't add features beyond what's requested
- Type safety: Let TypeScript guide you - all APIs are fully typed
- Validate inputs: Always use Valibot schemas for API inputs
- Error handling: Backend uses HTTPException, frontend uses toast notifications
Database Changes#
- Modify schema in
apps/api/src/database/schema.ts - Generate migration:
pnpm --filter @kaneo/api db:generate - Migration auto-runs on next API startup
- Always use CUID2 for IDs, include timestamps, specify cascade behavior
Adding API Endpoints#
- Create controller in
apps/api/src/{feature}/controllers/ - Add route in
apps/api/src/{feature}/index.ts - Use
describeRoutefor OpenAPI docs - Use
validatorwith Valibot schema - Keep route handler thin - business logic in controller
Adding Frontend Features#
- Create fetcher in
apps/web/src/fetchers/{feature}/ - Create query/mutation hook in
apps/web/src/hooks/ - Use TanStack Query for caching
- Handle loading/error states properly
- Use toast notifications (sonner) for user feedback
Important Notes#
- Package Manager: This project uses pnpm (pinned to
10.28.0viapackageManagerfield), 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_ORIGINSenv var - Testing: Run
pnpm testat the repo root (Turbo runstestin packages that define it: API unit tests, web unit/component tests, shared packages). API integration tests:pnpm test:integration(requires PostgreSQL; env is set intests/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 undertests/api-integration/; API unit tests undertests/api/. - Security: Never commit secrets, always validate inputs, sanitize outputs
Common Patterns#
Backend Route Example#
// 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#
// 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#
// 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),
]);