Best Practices for Developing with Kaneo: A Complete Guide to the Monorepo Architecture
The best practices for developing with Kaneo center on maintaining its pnpm monorepo structure, using TurboRepo for build orchestration, implementing thin Hono routes with Valibot validation, and following the controller pattern to separate business logic from HTTP handling.
Kaneo is a self-hosted project management platform built as a modern pnpm monorepo using TurboRepo. Developing effectively requires understanding its architectural conventions—from the controller-based API design in apps/api to the TanStack Query patterns in apps/web. This guide covers the essential practices derived from the Kaneo source code to keep your contributions clean, performant, and consistent with the codebase.
1. Maintain the Monorepo Structure with pnpm and TurboRepo
Kaneo organizes code into three main applications under apps/ (API, web, docs) plus shared libraries in packages/. Keep this structure intact when adding features.
The pnpm-workspace.yaml at the repository root defines workspace boundaries. Always run commands from the root so TurboRepo can orchestrate builds across the dependency graph:
pnpm dev # Launches API and web in watch mode
pnpm lint # Runs Biome linting across all packages
pnpm typecheck # Type-checks every package
pnpm build # Produces production bundles
Avoid running npm or yarn commands—pnpm is the required package manager for this workspace.
2. Architect the API Using Controllers and Validation
The API in apps/api uses Hono with a strict separation between routes and business logic.
Thin Routes and Controllers
Business logic belongs in controller files under apps/api/src/<feature>/controllers/. Routes should remain thin wrappers that apply validation, authentication, and OpenAPI metadata.
For example, apps/api/src/workspace/controllers/get-workspace-members.ts contains the database query logic, while the route handler only validates inputs and calls the controller:
// 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;
Input Validation with Valibot
Use Valibot (or Zod where required) for schema validation. The validator helper from hono-openapi attaches schemas to route parameters, body, or query parameters. This ensures type safety and automatic OpenAPI spec generation.
OpenAPI Documentation
Every route must use describeRoute to generate the OpenAPI specification. The spec generator lives in apps/api/src/utils/openapi-spec.ts. Documenting routes here enables the auto-generated API documentation.
Authentication Handling
Better Auth provides JWT-based authentication. Access the current user in request handlers via c.get("user") as implemented in apps/api/src/auth.ts. Always protect routes that require authentication using the appropriate middleware.
Database Conventions
Drizzle ORM models live in apps/api/src/database/schema.ts. Follow these conventions:
- Use CUID2 for all ID fields
- Include
createdAtandupdatedAttimestamps on all tables - Keep schema definitions in the central schema file
3. Build the Front-End with TanStack Query and Zustand
The web application in apps/web uses React with specific patterns for data fetching and state management.
Data Fetching Patterns
Encapsulate API calls in "fetchers" under apps/web/src/fetchers/, then consume them via custom hooks in apps/web/src/hooks/queries/.
For example, apps/web/src/fetchers/workspace/get-workspaces.ts handles the HTTP request:
// 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();
}
Then create a TanStack Query hook:
// 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));
}
State Management
Use Zustand for global UI state. Follow the existing store patterns in apps/web/src/lib/ to ensure consistency across features.
UI Components and Styling
Build UI components using Radix UI primitives and Tailwind v4. Place components in apps/web/src/components/ using PascalCase naming. This ensures accessibility and consistent styling across the application.
Internationalization Requirements
All user-visible strings must use react-i18next. Add keys to i18n/en-US.json first, then reference them via t("namespace:key") syntax.
Example implementation:
// i18n/en-US.json
{
"common": {
"actions": {
"close": "Close"
}
},
"tasks": {
"greeting": "Hello, {{name}}!"
}
}
// 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>;
}
See CONTRIBUTING.md for detailed i18n workflow instructions.
4. Configure Environment Variables Correctly
Kaneo uses a single .env file in the repository root to supply variables to both the API and web services.
Required variables include:
KANEO_CLIENT_URLKANEO_API_URLAUTH_SECRET- PostgreSQL credentials
See ENVIRONMENT_SETUP.md for the complete list.
CORS Configuration
During local development, you can leave CORS_ORIGINS empty. For production, set it to the exact origin of the web app. The API reads this variable in apps/api/src/config/index.ts.
Redis for Horizontal Scaling
For multi-instance deployments, configure Redis via REDIS_URL, Sentinel, or Cluster mode. The WebSocket broadcast adapters in apps/api/src/ws/ (in-memory-broadcast-adapter.ts and redis-broadcast-adapter.ts) handle this automatically based on environment variables.
5. Enforce Code Quality with Biome and Conventional Commits
Biome is the sole formatter and linter. Run pnpm lint before committing; the pre-commit hook enforces this automatically.
Style requirements:
- Double quotes everywhere
- Semicolons required
- Import ordering: external → internal (
@/alias) → relative
Commit messages must follow Conventional Commits (feat:, fix:, docs:, etc.). The commit-lint configuration lives in .husky/commit-msg.
6. Implement Comprehensive Testing
Maintain code quality through unit and integration tests.
Unit Testing
Vitest unit tests live alongside source files as *.test.ts. The API has its own vitest.config.ts under apps/api/; the web app has one under apps/web/.
Integration Testing
Integration tests use a real PostgreSQL instance and reside in tests/api-integration/. For example, tests/api-integration/project.test.ts validates end-to-end project workflows.
Run the full suite before pushing:
pnpm test # Unit tests
pnpm test:integration # Integration tests
Summary
- Maintain monorepo integrity by using pnpm commands from the root and respecting the
apps/andpackages/structure - Separate API concerns using controllers for business logic, Valibot for validation, and
describeRoutefor OpenAPI documentation - Follow front-end patterns by placing fetchers in
apps/web/src/fetchers/, TanStack Query hooks inapps/web/src/hooks/queries/, and using Zustand for global state - Configure environment variables in a root
.envfile, including optional Redis settings for scaling - Enforce code quality with Biome formatting, import ordering, and Conventional Commits
- Write tests using Vitest for unit tests and PostgreSQL-backed integration tests in
tests/api-integration/
Frequently Asked Questions
How do I add a new API endpoint in Kaneo?
Create a controller file in apps/api/src/<feature>/controllers/ containing your business logic. Then define a thin route in apps/api/src/<feature>/index.ts using Hono, applying Valibot validation via validator() and OpenAPI metadata via describeRoute(). This keeps HTTP handling separate from database queries.
What is the correct way to fetch data from the front-end?
Create a fetcher function in apps/web/src/fetchers/ that calls fetch() with credentials: "include". Then create a custom hook in apps/web/src/hooks/queries/ using useQuery from TanStack Query. Import the fetcher into your hook and component to consume the data.
How should I handle authentication in the API?
Use the Better Auth middleware to protect routes. Access the authenticated user in your handlers via c.get("user") as defined in apps/api/src/auth.ts. The middleware validates JWT tokens automatically, making user information available to downstream controllers.
What testing strategy does Kaneo require?
Write unit tests as *.test.ts files alongside your source code using Vitest. For end-to-end validation, add integration tests in tests/api-integration/ that test against a real PostgreSQL instance. Run pnpm test for unit tests and pnpm test:integration for integration tests before submitting changes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →