Performance Considerations for Kaneo: 6 Optimization Strategies for Self-Hosted Deployments

Kaneo's lightweight monorepo architecture—built with Hono, React 19, and Vite—offers multiple tuning levers for database queries, WebSocket scaling, caching, and asset handling to maintain high performance as your project management workload grows.

Kaneo (usekaneo/kaneo) is designed as a fast, self-hosted alternative to bloated SaaS project management tools. Its deliberate minimalism—Node.js API, React front-end, and PostgreSQL datastore—means performance bottlenecks are predictable and fixable. This guide covers the concrete optimization strategies embedded in Kaneo's source code, with specific file references and runnable configurations.


Database Schema Optimization with Drizzle ORM

PostgreSQL serves as Kaneo's single source of truth. Poor indexing or unbounded queries quickly become bottlenecks as project data accumulates.

Kaneo's schema in apps/api/src/database/schema.ts addresses this head-on:

  • Explicit indexes on foreign keys — Every relational column uses index("…_idx") for fast joins
  • CUID2 primary keys — Ultra-fast createId() from paralleldrive/cuid2 instead of UUIDv4
  • Automatic timestamps — $onUpdate and default values minimize extra UPDATE statements

Pagination Pattern for Large Collections

Avoid loading entire collections. Use LIMIT/OFFSET pagination as implemented in the project list controller:

// 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;
}

Action items:

  • Add index() declarations when introducing searchable columns
  • Monitor query plans with EXPLAIN ANALYZE for sequential scans
  • Consider cursor-based pagination for real-time infinite scroll scenarios

WebSocket Scaling: From In-Memory to Redis Pub/Sub

Real-time updates—task changes, comments, notifications—flow through WebSockets. Kaneo's broadcast architecture adapts to your deployment size.

Default behavior: Single-process in-memory broadcast (suitable for single-container deployments)

Production scaling: Set REDIS_URL to activate RedisBroadcastAdapter

The adaptive logic lives in apps/api/src/ws/broadcast.ts:

// Behavior verified in: tests/api/ws/broadcast.test.ts

Configuration is environment-driven:


# .env

REDIS_URL=redis://redis:6379

Redis tuning for Kaneo:

  • Set maxmemory-policy allkeys-lru or allkeys-lfu for predictable eviction
  • Enable persistence only if message durability matters (Kaneo broadcasts are ephemeral state syncs)
  • Monitor connected_clients and pubsub_channels metrics

Frontend Caching with TanStack Query

Kaneo's React 19 front-end in apps/web uses TanStack Query for automatic request deduplication and caching. Misconfigured query keys waste this benefit.

Pattern from apps/web/src/hooks/queries/project/use-project.ts:

// 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],  // Stable, hierarchical keys
    queryFn: () => listProjects(page),
    staleTime: 5 * 60_000,         // 5 minutes before refetch
    keepPreviousData: true,        // Smooth pagination transitions
  });
}

Caching best practices:

  • Structure keys hierarchically: ["project", projectId, "tasks"] not ["projectTasks", projectId]
  • Set staleTime based on data volatility—rarely-changing project metadata can cache longer
  • Invalidate precisely: queryClient.invalidateQueries({ queryKey: ["projects"] }) after mutations

Streaming Uploads for Task Images

Task attachments bypass the API server to reduce I/O latency. Kaneo generates pre-signed S3-compatible URLs for direct browser-to-storage uploads.

Implementation in 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);
  
  // Streams directly to S3-compatible storage
  await fetch(url, { method: "POST", body: form });
}

Optimization opportunities:

  • Enable multipart/resumable uploads for files >100MB
  • Configure CDN caching headers on your S3 bucket (Cache-Control: public, max-age=31536000)
  • Use image processing pipelines (lambda/edge functions) for automatic resizing

Build Optimization and Bundle Splitting

Kaneo's TurboRepo monorepo runs concurrent builds for API and web packages. Vite handles code-splitting for the React application.

Configuration in apps/web/vite.config.ts:

// Vite handles code-splitting automatically; manual chunks for vendor separation:
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Split heavy dependencies if bundle analysis shows bloat
          'vendor-react': ['react', 'react-dom'],
        },
      },
    },
  },
});

CI/CD performance:

  • pnpm build leverages Turbo's remote caching when available
  • Biome runs linting and autofix in parallel—no ESLint overhead
  • Keep server-only code free of heavy client libraries to reduce cold starts

Authentication Overhead and Session Caching

Better Auth validates JWTs on every request using HMAC-SHA256 (fast by design). The implementation in apps/api/src/auth/better-auth.ts uses a 32-byte AUTH_SECRET:

AUTH_SECRET=your-random-32-byte-string

For high-traffic deployments, add Redis session caching:

// With REDIS_URL set, extend better-auth with session storage adapter
// Reduces database lookups per request from 1-2 queries to 0

Token lifecycle tuning:

  • Keep access token lifetimes short (15-60 minutes)
  • Rotate AUTH_SECRET only during maintenance windows—invalidates all sessions
  • Cache user permissions in Redis with TTL matching token lifetime

Summary

  • Index foreign keys and paginate aggressively — Kaneo's Drizzle schema provides the foundation; add indexes for your custom queries
  • Scale WebSockets horizontally with REDIS_URL — Single environment variable enables multi-instance deployments
  • Leverage TanStack Query's cache hierarchy — Stable keys and appropriate staleTime eliminate redundant requests
  • Stream uploads directly to object storage — Pre-signed URLs keep the API responsive under file upload load
  • Monitor bundle size and build times — Vite and Turbo provide the tools; profile before optimizing
  • Cache sessions in Redis for high-RPS APIs — Optional but impactful for authentication-heavy workloads

Frequently Asked Questions

Does Kaneo require Redis for production use?

No—Redis is optional but strongly recommended for multi-container deployments. Without REDIS_URL, WebSocket broadcasts remain in-memory and confined to a single process. For single-instance Docker deployments, this is sufficient. The test suite in tests/api/ws/broadcast.test.ts validates both configurations.

How does Kaneo handle large file uploads without server timeouts?

The API never buffers file contents. Instead, apps/web/src/lib/upload-task-image.ts requests a pre-signed S3-compatible URL from the API, then streams the file directly from the browser to object storage. This bypasses Node.js memory constraints and keeps API response times consistent regardless of file size.

What database performance metrics should I monitor for Kaneo?

Focus on PostgreSQL slow query logs for sequential scans on large tables, connection pool saturation (default pools vary by hosting), and table bloat on frequently-updated tables like taskTable and commentTable. Kaneo's CUID2 primary keys and indexed foreign keys in apps/api/src/database/schema.ts provide a performant baseline.

Can I use a CDN with Kaneo's uploaded images?

Yes—configure your S3-compatible storage bucket with a custom domain and CloudFlare, AWS CloudFront, or similar. Kaneo stores and serves image URLs as opaque strings; the pre-signed upload flow in upload-task-image.ts accepts any S3-compatible endpoint. Set long cache headers on the bucket and invalidate via API when images are replaced.

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 →