Kaneo Performance Tuning: A Complete Guide to Database, Real-Time, and API Optimization

Kaneo achieves high performance through strategic PostgreSQL indexing, Redis-backed pub/sub for WebSocket scaling, lightweight Hono request handling, and intelligent TanStack Query caching.

Kaneo is an open-source project management tool built as a pnpm monorepo that separates concerns between a backend API (apps/api) and a React frontend (apps/web). This article examines the specific architectural decisions and code patterns that enable Kaneo performance tuning across three critical layers: database access, real-time updates, and API efficiency. All findings derive directly from the usekaneo/kaneo source code.

Database Layer: PostgreSQL with Drizzle ORM

Kaneo's data persistence relies on PostgreSQL accessed through Drizzle ORM. The schema definition lives in a single comprehensive file where performance optimizations are declared explicitly.

Strategic Index Usage

In /apps/api/src/database/schema.ts, indexes target the exact query patterns the application generates:

// Foreign key indexes for join performance
index("task_projectId_idx").on(taskTable.projectId),
index("task_dueDate_idx").on(taskTable.dueDate),
index("workspace_member_userId_idx").on(workspaceMemberTable.userId),

// Composite unique constraints for data integrity
uniqueIndex("task_project_number_unique").on(
  taskTable.projectId,
  taskTable.taskNumber
),

These indexes enable index-only scans instead of full table scans when filtering by projectId, workspaceId, or user-related columns. The task_project_number_unique constraint additionally prevents expensive duplicate-checking logic in application code.

Cascade Rules for Cheap Referential Integrity

Deletion and update operations use database-level cascades defined in the schema:

references(() => projectTable.id, { onDelete: "cascade" })

This pushes referential integrity work to PostgreSQL's optimized engine rather than requiring multiple round-trip queries from the application layer.

Pagination in Query Controllers

The get-tasks.ts controller demonstrates limit/offset pagination that prevents unbounded result sets:

// apps/api/src/task/controllers/get-tasks.ts
export async function getTasks({ projectId, limit, offset }) {
  return db
    .select()
    .from(taskTable)
    .where(eq(taskTable.projectId, projectId))
    .limit(limit)
    .offset(offset);
}

Key takeaway: Always paginate list endpoints. The limit and offset parameters are enforced at the database level, not just as API conventions.

Real-Time Layer: Redis Pub/Sub for WebSocket Scaling

Kaneo's WebSocket infrastructure supports horizontal scaling through a pluggable broadcast adapter pattern.

Redis Broadcast Adapter Architecture

The RedisBroadcastAdapter in /apps/api/src/ws/redis-broadcast-adapter.ts replaces the default in-memory adapter when Redis is available:

// Publishing to all subscribed servers
async broadcast(event: string, data: unknown) {
  const redisPub = await getRedisPub();
  await redisPub.publish(
    CHANNEL_PATTERN,
    JSON.stringify({ event, data, senderId: this.serverId })
  );
}

Each server instance subscribes to the same CHANNEL_PATTERN. Messages published by any instance reach all others through Redis, eliminating the need for inter-server polling or sticky sessions.

Lazy Connection Management

Redis connections are created on-demand and properly closed on shutdown:

// apps/api/src/redis/index.ts
export async function getRedisPub() {
  if (!redisPub) {
    redisPub = createClient({ url: getRedisUrl() });
    await redisPub.connect();
  }
  return redisPub;
}

export async function closeRedis() {
  await redisPub?.quit();
  await redisSub?.quit();
  redisPub = redisSub = undefined;
}

Configuration requirement: Set REDIS_URL (or REDIS_HOST/REDIS_PORT) so isRedisConfigured() returns true. Without this, the system falls back to the in-memory BroadcastAdapter, which works for single-instance deployments but breaks real-time sync across multiple servers.

API Layer: Hono and Valibot for Minimal Overhead

The API server prioritizes request processing speed through lightweight dependencies.

Hono's Minimal Middleware Stack

Unlike heavier frameworks, Hono processes HTTP requests with minimal allocation and middleware traversal. Route handlers remain thin, delegating to controller functions:

// Route definition delegates to controller
app.get("/api/tasks", async (c) => {
  const { projectId, limit, offset } = parseQuery(c.req.query());
  const tasks = await getTasks({ projectId, limit, offset });
  return c.json(tasks);
});

Valibot for Zero-Cost Validation

Input validation uses Valibot, which validates without instantiating heavy schema objects:

import * as v from "valibot";

const TaskQuerySchema = v.object({
  projectId: v.string(),
  limit: v.number(v.integer(), v.minValue(1), v.maxValue(100)),
  offset: v.number(v.integer(), v.minValue(0)),
});

export function parseQuery(input: unknown) {
  return v.parse(TaskQuerySchema, input);
}

This keeps garbage collection pressure low during high-throughput scenarios.

Frontend Layer: TanStack Query for Intelligent Caching

The React frontend in apps/web leverages TanStack Query to minimize redundant API calls and smooth user interactions.

Pagination with Data Prefetching

The useTasks hook implements stale-while-revalidate caching with pagination support:

import { useQuery } from "@tanstack/react-query";
import { getTasks } from "@/fetchers/task/get-tasks";

export function useTasks(projectId: string, page: number, pageSize: number) {
  return useQuery({
    queryKey: ["tasks", projectId, page],
    queryFn: () => getTasks({
      projectId,
      limit: pageSize,
      offset: page * pageSize
    }),
    keepPreviousData: true, // Smooths page transitions
  });
}

The keepPreviousData: true option displays stale data while fetching new results, eliminating loading spinners during pagination.

Proactive Prefetching

Navigation performance improves by prefetching data before the user requests it:

// Prefetch next page when hovering pagination control
queryClient.prefetchQuery({
  queryKey: ["tasks", projectId, nextPage],
  queryFn: () => getTasks({
    projectId,
    limit: pageSize,
    offset: nextPage * pageSize
  }),
});

Cache Tuning for Immutable Data

Data that changes infrequently (workspace lists, user profiles) benefits from extended stale times:

useQuery({
  queryKey: ["workspaces"],
  queryFn: fetchWorkspaces,
  staleTime: 5 * 60 * 1000, // 5 minutes
});

Practical Performance Checklist

Apply these specific actions to optimize your Kaneo deployment:

  1. Verify indexes for new filters — Before deploying query changes, confirm schema.ts contains matching indexes. Add them with index("table_column_idx").on(table.column) if absent.

  2. Enforce pagination everywhere — API endpoints accepting list parameters must implement limit/offset. Default to reasonable maximums (e.g., 100 records).

  3. Configure Redis in production — Set environment variables so isRedisConfigured() activates the Redis broadcast adapter. Essential for multi-instance deployments.

  4. Extend staleTime for stable data — Identify slowly-changing entities and increase their TanStack Query staleTime to reduce API traffic.

  5. Eliminate N+1 queries — Use Drizzle's relation helpers and explicit joins instead of looping through results with individual queries:

// Preferred: single query with join
await db
  .select()
  .from(taskTable)
  .where(eq(taskTable.projectId, projectId))
  .leftJoin(userTable, eq(taskTable.assigneeId, userTable.id));

// Avoid: querying users separately for each task

Summary

  • Database performance relies on explicit indexes in schema.ts, pagination in controllers, and cascade rules for efficient deletions.
  • Real-time scaling requires Redis configuration to activate the RedisBroadcastAdapter, enabling horizontal WebSocket distribution.
  • API efficiency comes from Hono's minimal overhead and Valibot's lightweight validation.
  • Frontend responsiveness benefits from TanStack Query's automatic caching, prefetching, and configurable stale times.

Frequently Asked Questions

How do I enable Redis for WebSocket scaling in Kaneo?

Set the REDIS_URL environment variable (or REDIS_HOST and REDIS_PORT separately) before starting the API server. The isRedisConfigured() function in /apps/api/src/redis/index.ts detects this configuration and automatically instantiates the RedisBroadcastAdapter instead of the in-memory fallback.

What database indexes does Kaneo use for task queries?

Kaneo defines task_projectId_idx and task_dueDate_idx in schema.ts to accelerate filtering by project and due date. The composite unique index task_project_number_unique also optimizes lookups by task number within a project while enforcing uniqueness.

Why does Kaneo use Valibot instead of Zod for validation?

Valibot provides equivalent type-safe validation without the runtime overhead of instantiating schema objects. This reduces memory allocation per request, which compounds into meaningful performance gains under high throughput.

How does the frontend prevent loading states during pagination?

The useTasks hook sets keepPreviousData: true in its TanStack Query configuration. This displays the previous page's data while fetching the new page, eliminating visible loading states and improving perceived performance.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →