# Kaneo Development Best Practices: A Complete Guide to Contributing to the Self-Hosted Project Management Platform

> Learn Kaneo development best practices for contributing to the self-hosted project management platform. Master monorepo structure, controller pattern, Valibot, TanStack Query, Biome, and Conventional Commits.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: best-practices
- Published: 2026-08-05

---

**The best practices for developing with Kaneo include maintaining the pnpm monorepo structure, using the controller pattern for API logic, validating inputs with Valibot, consuming data via TanStack Query fetchers on the frontend, and enforcing code quality through Biome linting and Conventional Commits.**

Kaneo is a self-hosted project management platform built as a modern TypeScript monorepo. Whether you're extending the API, building new UI features, or deploying to production, following the established conventions in the `usekaneo/kaneo` repository ensures your contributions remain maintainable and consistent with the codebase. This guide covers the architectural patterns, tooling, and workflows that govern development across the API, web frontend, and shared packages.

## Monorepo Structure and Package Management

Kaneo organizes code into a **pnpm workspace** orchestrated by **TurboRepo**. Understanding this layout is essential before making any changes.

### Workspace Organization

The repository root contains three primary directories:

- `apps/` – Runnable services (API, web, documentation)
- `packages/` – Reusable libraries shared across apps
- `charts/` – Helm charts for Kubernetes deployment

This structure is documented in [[`CLAUDE.md`](https://github.com/usekaneo/kaneo/blob/main/CLAUDE.md)](https://github.com/usekaneo/kaneo/blob/main/CLAUDE.md) and enforced by [[`pnpm-workspace.yaml`](https://github.com/usekaneo/kaneo/blob/main/pnpm-workspace.yaml)](https://github.com/usekaneo/kaneo/blob/main/pnpm-workspace.yaml), which defines workspace boundaries.

### Command Orchestration

Always run commands from the repository root so TurboRepo can parallelize builds and cache results:

```bash
pnpm dev          # Launches API and web in watch mode

pnpm lint         # Runs Biome linting with auto-fix

pnpm typecheck    # Type-checks every package

pnpm build        # Produces production bundles

```

## API Development Patterns

The Kaneo API in `apps/api` follows a **controller pattern** that separates business logic from route handling, with strict input validation and automatic OpenAPI documentation.

### Controller-Based Architecture

Place business logic in `apps/api/src/<feature>/controllers/` and keep route files thin. For example, [[`apps/api/src/workspace/controllers/get-workspace-members.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/workspace/controllers/get-workspace-members.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/workspace/controllers/get-workspace-members.ts) demonstrates a clean database query pattern:

```typescript
// Controllers contain pure business logic
const getWorkspaceMembers = async (workspaceId: string) => {
  // Database interaction via Drizzle ORM
  return db.query.workspaceMembers.findMany({
    where: eq(workspaceMembers.workspaceId, workspaceId),
  });
};

```

Route handlers apply validation, authentication, and OpenAPI metadata:

```typescript
// apps/api/src/example/index.ts
import { Hono } from "hono";
import { describeRoute, validator } from "hono-openapi";
import * as v from "valibot";
import getExample from "./controllers/get-example";

const example = new Hono<{ Variables: { userId: string } }>()
  .get(
    "/:id",
    describeRoute({
      operationId: "getExample",
      tags: ["Example"],
      description: "Fetch an example resource by ID",
    }),
    validator("param", v.object({ id: v.string() })),
    async (c) => {
      const { id } = c.req.valid("param");
      const result = await getExample(id);
      return c.json(result);
    },
  );

export default example;

```

### Validation and Documentation

- **Valibot** (preferred) or Zod for schema validation via the `validator` helper from `hono-openapi`
- **OpenAPI specs** generated automatically through `describeRoute` calls; the generator lives in [[`apps/api/src/utils/openapi-spec.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/openapi-spec.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/openapi-spec.ts)

### Authentication and Database

Access the current user in request handlers via `c.get("user")`, provided by Better Auth as configured in [[`apps/api/src/auth.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/auth.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/auth.ts).

Database models use **Drizzle ORM** in [[`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts), with conventions including:
- **CUID2** identifiers for all primary keys
- `createdAt` and `updatedAt` timestamps on every table

## Frontend Development with React and TanStack Query

The Kaneo web app (`apps/web`) follows predictable patterns for data fetching, state management, and component structure.

### Data Fetching Architecture

Encapsulate API calls in **fetchers** under `apps/web/src/fetchers/`, then consume them through **custom hooks** in `apps/web/src/hooks/queries/`:

```typescript
// apps/web/src/fetchers/example/get-example.ts
import { apiUrl } from "@/lib/api-url";

export async function getExample(id: string) {
  const res = await fetch(`${apiUrl}/example/${id}`, {
    credentials: "include",
  });
  if (!res.ok) throw new Error("Failed to fetch example");
  return res.json();
}

```

```typescript
// apps/web/src/hooks/queries/example/useExample.ts
import { useQuery } from "@tanstack/react-query";
import { getExample } from "@/fetchers/example/get-example";

export function useExample(id: string) {
  return useQuery(["example", id], () => getExample(id));
}

```

See [[`apps/web/src/fetchers/workspace/get-workspaces.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/fetchers/workspace/get-workspaces.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/fetchers/workspace/get-workspaces.ts) for a production fetcher example.

### State Management and UI

- **Zustand** for global UI state, following patterns in `apps/web/src/lib/`
- **Radix UI primitives** with **Tailwind CSS v4** for accessible components
- PascalCase naming convention; place components in `apps/web/src/components/`

### Internationalization Requirements

All user-visible strings must use **react-i18next**. Add keys to [[`i18n/en-US.json`](https://github.com/usekaneo/kaneo/blob/main/i18n/en-US.json)](https://github.com/usekaneo/kaneo/blob/main/i18n/en-US.json) first, then reference via the `t()` function:

```typescript
// apps/web/src/components/TaskGreeting.tsx
import { useTranslation } from "react-i18next";

export function TaskGreeting({ name }: { name: string }) {
  const { t } = useTranslation();
  return <p>{t("tasks:greeting", { name })}</p>;
}

```

The i18n workflow is detailed in [[`CONTRIBUTING.md`](https://github.com/usekaneo/kaneo/blob/main/CONTRIBUTING.md)](https://github.com/usekaneo/kaneo/blob/main/CONTRIBUTING.md).

## Environment Configuration

Kaneo uses a **single `.env` file** at the repository root for both API and web services. Required variables include:

| Variable | Purpose |
|----------|---------|
| `KANEO_CLIENT_URL` | Web app origin for redirects and CORS |
| `KANEO_API_URL` | Public API endpoint |
| `AUTH_SECRET` | Encryption key for Better Auth |
| PostgreSQL credentials | Database connection |

Complete documentation appears in [[`ENVIRONMENT_SETUP.md`](https://github.com/usekaneo/kaneo/blob/main/ENVIRONMENT_SETUP.md)](https://github.com/usekaneo/kaneo/blob/main/ENVIRONMENT_SETUP.md).

### CORS and Scaling

- **CORS**: Leave `CORS_ORIGINS` empty for local development; set to the exact web origin in production (read from [`apps/api/src/config/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/config/index.ts))
- **Redis**: Optional for horizontal scaling; configure single mode, Sentinel, or Cluster via `REDIS_URL`. WebSocket broadcast adapters live in `apps/api/src/ws/`—compare [[`in-memory-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/in-memory-broadcast-adapter.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/in-memory-broadcast-adapter.ts) and [[`redis-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/redis-broadcast-adapter.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/redis-broadcast-adapter.ts)

## Code Quality and Testing Standards

### Linting and Formatting with Biome

**Biome** is the sole tool for formatting and linting. Key rules enforced:

- Double quotes everywhere
- Semicolons required
- Import ordering: external → internal (`@/` alias) → relative

Run `pnpm lint` before committing; a pre-commit hook enforces this automatically.

### Conventional Commits

Follow the Conventional Commits specification (`feat:`, `fix:`, `docs:`, etc.). The commit-msg hook in `.husky/commit-msg` validates format.

### Testing Strategy

| Test Type | Location | Command |
|-----------|----------|---------|
| Unit tests | Alongside source (`*.test.ts`) | `pnpm test` |
| Integration tests | `tests/api-integration/` | `pnpm test:integration` |

Each app maintains its own Vitest configuration: [`apps/api/vitest.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/vitest.config.ts) and [`apps/web/vitest.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/vitest.config.ts). Integration tests use real PostgreSQL instances; see [[`tests/api-integration/project.test.ts`](https://github.com/usekaneo/kaneo/blob/main/tests/api-integration/project.test.ts)](https://github.com/usekaneo/kaneo/blob/main/tests/api-integration/project.test.ts) for an example.

## Key Files and References

| Area | File | Purpose |
|------|------|---------|
| Monorepo config | [[`pnpm-workspace.yaml`](https://github.com/usekaneo/kaneo/blob/main/pnpm-workspace.yaml)](https://github.com/usekaneo/kaneo/blob/main/pnpm-workspace.yaml) | Workspace boundaries |
| API entry | [[`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts) | Hono setup, middleware, routes |
| Controller pattern | [[`apps/api/src/workspace/controllers/get-workspace-members.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/workspace/controllers/get-workspace-members.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/workspace/controllers/get-workspace-members.ts) | DB query example |
| OpenAPI generator | [[`apps/api/src/utils/openapi-spec.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/openapi-spec.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/openapi-spec.ts) | Spec construction |
| Fetcher pattern | [[`apps/web/src/fetchers/workspace/get-workspaces.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/fetchers/workspace/get-workspaces.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/fetchers/workspace/get-workspaces.ts) | API client usage |
| Query hook | [[`apps/web/src/hooks/queries/workspace/useWorkspaces.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/queries/workspace/useWorkspaces.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/queries/workspace/useWorkspaces.ts) | TanStack Query integration |
| Shared utilities | [[`packages/libs/src/api-url.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/libs/src/api-url.ts)](https://github.com/usekaneo/kaneo/blob/main/packages/libs/src/api-url.ts) | Common API URL logic |

## Summary

- **Preserve monorepo structure**: Use pnpm and TurboRepo commands from the repository root
- **Separate concerns**: Controllers for business logic, thin Hono routes for validation and auth, fetchers with TanStack Query on the frontend
- **Validate rigorously**: Valibot schemas on all inputs, OpenAPI documentation on all routes
- **Share code appropriately**: Place reusable utilities in `packages/libs`
- **Configure environments correctly**: Single `.env` file, proper CORS origins, optional Redis for scaling
- **Maintain quality standards**: Biome formatting, Conventional Commits, comprehensive test coverage

## Frequently Asked Questions

### What package manager does Kaneo require?

Kaneo requires **pnpm**. The [`pnpm-workspace.yaml`](https://github.com/usekaneo/kaneo/blob/main/pnpm-workspace.yaml) file defines workspace boundaries, and all development commands (`pnpm dev`, `pnpm lint`, `pnpm build`) must be run from the repository root so TurboRepo can orchestrate the monorepo correctly.

### How should I structure a new API endpoint in Kaneo?

Create a controller file in `apps/api/src/<feature>/controllers/` containing your database logic, then wire it into a thin Hono route in `apps/api/src/<feature>/index.ts`. Apply Valibot validation via the `validator` helper and document the route with `describeRoute` for automatic OpenAPI generation.

### What is the recommended pattern for fetching data in the Kaneo frontend?

Encapsulate API calls in **fetcher functions** under `apps/web/src/fetchers/`, then consume them through **custom hooks** in `apps/web/src/hooks/queries/` using TanStack Query. This separation keeps data fetching logic reusable and caching behavior explicit.

### How do I add translations to the Kaneo interface?

Add new keys to [[`i18n/en-US.json`](https://github.com/usekaneo/kaneo/blob/main/i18n/en-US.json)](https://github.com/usekaneo/kaneo/blob/main/i18n/en-US.json) following the nested namespace structure, then reference them via `t("namespace:key")` from the `useTranslation` hook. All user-visible strings must go through react-i18next—hardcoded text is not permitted.

### What testing is required before submitting a Kaneo contribution?

Run `pnpm test` for unit tests and `pnpm test:integration` for integration tests. Unit tests live alongside source files, while integration tests requiring PostgreSQL reside in `tests/api-integration/`. The pre-commit hook also ensures Biome linting passes and commit messages follow Conventional Commits.