Performance Considerations for Kaneo: Optimizing Your Self-Hosted Project Management Platform

Kaneo delivers high performance through lightweight architecture choices including Drizzle ORM with indexed PostgreSQL schemas, Redis-backed WebSocket scaling, TanStack Query deduplication, and S3-compatible asset streaming.

Kaneo is a lightweight, self-hosted project management platform built as a monorepo with a Node.js API (Hono) and React 19 frontend (Vite). Understanding the performance considerations for Kaneo ensures your deployment remains responsive as team size and project complexity grow. The codebase deliberately avoids heavyweight abstractions, providing clear optimization levers across database queries, real-time communication, and asset handling.

Database Schema and Query Optimization

PostgreSQL serves as the single source of truth in Kaneo, making schema design and query efficiency critical for scaling. The platform uses Drizzle ORM with explicitly defined indexes on foreign-key columns to prevent table scans as project data accumulates.

In apps/api/src/database/schema.ts, primary keys utilize CUID2 (createId()) for ultra-fast generation and collision resistance. Foreign-key relationships include explicit index("…_idx") declarations to accelerate join operations. Implement LIMIT-based pagination instead of loading entire collections to maintain sub-100ms query times on large datasets.

// apps/api/src/project/controllers/list-projects.ts
import { db } from "@/database";
import { projectTable } from "@/database/schema";
import { eq, desc } from "drizzle-orm";

export async function listProjects(userId: string, page = 1, pageSize = 20) {
  const offset = (page - 1) * pageSize;
  const projects = await db
    .select()
    .from(projectTable)
    .where(eq(projectTable.ownerId, userId))
    .orderBy(desc(projectTable.createdAt))
    .limit(pageSize)
    .offset(offset);
  return projects;
}

Leverage Drizzle's $onUpdate and default timestamp handling to minimize unnecessary UPDATE statements. Add indexes immediately when introducing searchable columns to prevent sequential scans on growing tables.

WebSocket Scaling and Real-Time Performance

Real-time task updates and comments rely on WebSocket broadcasts that require different strategies depending on deployment scale. By default, Kaneo uses an in-memory broadcast adapter suitable for single-instance deployments, as verified in tests/api/ws/broadcast.test.ts.

For horizontal scaling across multiple containers, configure the Redis pub/sub adapter by setting REDIS_URL in your environment. When present, apps/api/src/ws/broadcast.ts automatically instantiates RedisBroadcastAdapter, enabling event synchronization across API instances.


# .env

REDIS_URL=redis://redis:6379

Deploy a dedicated Redis instance and tune the maxmemory-policy eviction strategy to prevent memory pressure during high-frequency update bursts. This eliminates the single-process bottleneck while maintaining sub-millisecond latency for real-time notifications.

Frontend Caching and Query Deduplication

The React frontend utilizes TanStack Query to eliminate redundant network requests through automatic deduplication and intelligent caching. Proper cache configuration prevents re-fetching identical data across components, reducing bandwidth consumption and server load.

Structure query keys hierarchically (e.g., ["project", projectId]) and configure staleTime and cacheTime parameters based on data volatility. After mutations, invalidate only affected queries to maintain cache consistency without triggering full refetches.

// apps/web/src/hooks/queries/project/use-projects.ts
import { useQuery } from "@tanstack/react-query";
import { listProjects } from "@/fetchers/project/list-projects";

export function useProjects(page: number) {
  return useQuery({
    queryKey: ["projects", page],
    queryFn: () => listProjects(page),
    staleTime: 5 * 60_000, // 5 minutes
    keepPreviousData: true,
  });
}

Set staleTime to 300,000ms (5 minutes) for relatively static project metadata while using shorter windows for rapidly changing task lists. This pattern, implemented in apps/web/src/hooks/queries/project/use-project.ts, minimizes API round-trips during navigation.

Static Asset Handling and Upload Performance

Task image uploads utilize S3-compatible storage with pre-signed URLs to bypass the API server for file I/O. The client streams files directly to object storage via apps/web/src/lib/upload-task-image.ts, eliminating memory pressure on the Node.js process.

// apps/web/src/lib/upload-task-image.ts
import { getPresignedUploadUrl } from "@/fetchers/task/get-presigned-url";

export async function uploadTaskImage(file: File, taskId: string) {
  const { url, fields } = await getPresignedUploadUrl(taskId);
  const form = new FormData();
  Object.entries(fields).forEach(([k, v]) => form.append(k, v));
  form.append("file", file);
  await fetch(url, { method: "POST", body: form });
}

Enable multipart upload for files exceeding 5MB to improve reliability on unstable connections. Configure CDN caching headers on your S3-compatible backend to serve task attachments with edge caching, reducing latency for distributed teams.

Build Optimization and Bundle Configuration

The monorepo structure uses TurboRepo to orchestrate concurrent API and web server builds. Vite handles code-splitting for the React 19 frontend, with configuration defined in apps/web/vite.config.ts. Biome provides fast linting and autofixing during CI pipelines.

Keep server-only code free of heavy client-side libraries to minimize cold-start times. Use Vite's manualChunks configuration to split vendor bundles if initial load times exceed performance budgets. Avoid importing visualization or charting libraries in API routes to prevent unnecessary memory overhead.

Authentication Overhead and Session Management

Better Auth validates JWT tokens on every request using HMAC-SHA256 verification with a 32-byte secret (AUTH_SECRET). While cryptographic verification is computationally efficient, database lookups for session validation can introduce latency under high concurrency.

Cache authenticated sessions in Redis when REDIS_URL is configured to eliminate repeated database queries. Set short token lifetimes (15-30 minutes) with refresh token rotation scheduled during low-traffic windows. The authentication implementation in apps/api/src/auth/better-auth.ts uses optimized JWT verification that typically adds less than 1ms per request.

Summary

  • Index foreign keys and searchable columns in apps/api/src/database/schema.ts using Drizzle's explicit index declarations to prevent table scans.
  • Scale WebSockets horizontally by setting REDIS_URL to enable the Redis pub/sub broadcast adapter across multiple API instances.
  • Configure TanStack Query caching with stable query keys and appropriate staleTime values to reduce redundant network traffic.
  • Stream large uploads through S3 pre-signed URLs via apps/web/src/lib/upload-task-image.ts to minimize API server load.
  • Keep dependencies lean in server code to maintain fast cold-start times and efficient memory usage.

Frequently Asked Questions

How does Kaneo handle WebSocket scaling across multiple instances?

By default, Kaneo uses an in-memory broadcast adapter suitable for single-container deployments. When you provide a REDIS_URL environment variable, apps/api/src/ws/broadcast.ts automatically switches to a Redis pub/sub adapter, enabling event synchronization across horizontally scaled API instances.

What database indexing strategy does Kaneo use?

Kaneo defines all tables in apps/api/src/database/schema.ts with explicit indexes on foreign-key columns using Drizzle ORM's index("…_idx") syntax. Primary keys use CUID2 for fast generation. You should add additional indexes when introducing new searchable fields to maintain query performance.

How can I optimize image uploads in Kaneo?

The platform streams task images directly to S3-compatible storage using pre-signed URLs generated via apps/web/src/lib/upload-task-image.ts. Enable multipart uploads for large files and configure CDN caching on your object storage to reduce delivery latency.

Does Kaneo support horizontal scaling?

Yes. Kaneo supports horizontal scaling through Redis-backed WebSocket broadcasting and stateless API design. Deploy multiple API container instances behind a load balancer, configure REDIS_URL for shared state, and use PostgreSQL for persistent storage to scale beyond single-node limitations.

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 →