Kaneo Backend Technology Stack: Node.js, TypeScript, Hono, and Drizzle ORM Explained
Kaneo's backend runs on Node.js 20+ with TypeScript, using the Hono web framework, Drizzle ORM with PostgreSQL, and optional Redis for real-time WebSocket broadcasting.
Kaneo is an open-source project management platform built as a modern, self-hostable monorepo. This article breaks down the core technology stack for Kaneo backend based on the actual source code in the usekaneo/kaneo repository, examining how each layer works together to power the API.
Runtime and Language Foundation
Node.js 20 as ES Modules
The Kaneo backend executes as an ES module on Node.js 20 or later. This modern runtime provides the JavaScript engine, standard library, and native support for import syntax without transpilation overhead in production.
In apps/api/package.json, the configuration explicitly declares the module type:
{
"type": "module",
"engines": { "node": ">=20" }
}
TypeScript with esbuild
TypeScript provides static typing across the entire backend. Rather than the traditional tsc compiler, Kaneo uses esbuild for fast bundling. This choice minimizes build times while preserving type safety through editor integration and CI checks.
Web Framework: Hono
Why Hono?
The Hono framework (hono, @hono/node-server, @hono/node-ws) serves as the HTTP and WebSocket layer. Hono offers:
- Minimal overhead — lighter than Express or Fastify
- First-class TypeScript — end-to-end type inference
- Middleware ecosystem — built-in CORS, compression, and caching
- WebSocket support — via
@hono/node-wsfor real-time features
Server Bootstrap in index.ts
The main application entry point in apps/api/src/index.ts demonstrates the complete Hono setup:
import { OpenAPIHono } from "@hono/zod-openapi";
import { serve } from "@hono/node-server";
import { createNodeWebSocket } from "@hono/node-ws";
const app = new OpenAPIHono();
// Middleware stack
app.use(compress());
app.use(cors());
app.use(secureHeaders());
// WebSocket upgrade handling
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
serve(app, (info) => {
console.log(`Server running on ${info.address}:${info.port}`);
});
This single file configures routing, OpenAPI documentation, compression, CORS headers, and the WebSocket upgrade path.
API Design: OpenAPI and Zod Validation
@hono/zod-openapi Integration
Kaneo generates OpenAPI 3.x specifications automatically from route definitions using @hono/zod-openapi. Each endpoint declares its request/response schemas with Zod, enabling:
- Runtime validation of request bodies, query params, and path segments
- Auto-generated API documentation
- Type inference from schemas to handler contexts
Example route with full schema definition:
import { OpenAPIHono, z } from "@hono/zod-openapi";
const api = new OpenAPIHono();
api.get(
"/projects/:projectId",
{
request: {
params: z.object({ projectId: z.string().uuid() }),
},
responses: {
200: {
description: "Project details",
content: {
"application/json": {
schema: z.object({
id: z.string().uuid(),
name: z.string(),
createdAt: z.string().datetime(),
}),
},
},
},
404: { description: "Project not found" },
},
},
async (c) => {
const { projectId } = c.req.valid("param");
const project = await getProject(projectId);
return project ? c.json(project) : c.notFound();
}
);
The c.req.valid("param") call provides typed, validated parameters—no manual parsing required.
Authentication Layer
better-auth for Session and API Key Management
better-auth (better-auth, @better-auth/api-key) handles all authentication concerns:
- Session management with secure cookies
- API key authentication for programmatic access
- Password hashing via bcryptjs
- OAuth provider integrations (GitHub, Gitea, Discord, Slack, Telegram)
The modular design allows Kaneo to enable only the authentication methods needed per deployment.
Database: PostgreSQL with Drizzle ORM
Drizzle as the Query Builder
Drizzle ORM provides a strongly-typed SQL-like query builder that compiles to efficient PostgreSQL queries. Unlike heavyweight ORMs, Drizzle stays close to SQL semantics while offering complete type safety.
Connection and Schema Setup
In apps/api/src/database/index.ts, the database layer initializes:
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
});
export const db = drizzle(pool, { schema });
// Lazy-loaded proxy for hot-reload compatibility
export const getDb = () => db;
Query Example with Type Inference
import { eq, and } from "drizzle-orm";
import { taskTable, projectTable } from "./database/schema";
export async function getTasksByProject(projectId: string, status?: string) {
const conditions = [eq(taskTable.projectId, projectId)];
if (status) {
conditions.push(eq(taskTable.status, status));
}
return await db
.select()
.from(taskTable)
.where(and(...conditions))
.orderBy(taskTable.createdAt);
}
The drizzle-orm import provides SQL operators (eq, and, or, inArray, etc.) that map directly to PostgreSQL operators.
Migrations with drizzle-kit
Database schema changes are managed via drizzle-kit, which generates and runs migration files. The schema definitions in apps/api/src/database/schema.ts serve as the single source of truth.
Real-Time Layer: WebSockets with Optional Redis
Broadcast Architecture
Kaneo supports real-time updates through WebSockets, with a pluggable broadcast system for scaling:
- In-memory adapter — for single-instance deployments
- Redis adapter — for horizontal scaling across multiple API instances
In-Memory Broadcast Adapter
apps/api/src/ws/in-memory-broadcast-adapter.ts maintains a local Map of connected clients:
const clients = new Map<string, WebSocket>();
export function broadcast(event: string, payload: unknown) {
const message = JSON.stringify({ event, payload });
for (const ws of clients.values()) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
}
}
Redis Broadcast Adapter
apps/api/src/ws/redis-broadcast-adapter.ts uses ioredis to subscribe to a Redis channel and forward messages to local WebSocket clients:
import Redis from "ioredis";
const pub = new Redis(process.env.REDIS_URL);
const sub = new Redis(process.env.REDIS_URL);
sub.subscribe("kaneo:events");
sub.on("message", (channel, message) => {
const { event, payload } = JSON.parse(message);
// Forward to all local WebSocket connections
localBroadcast(event, payload);
});
export function broadcast(event: string, payload: unknown) {
pub.publish("kaneo:events", JSON.stringify({ event, payload }));
}
This design lets operators start with a simple deployment and add Redis only when scaling demands it.
Observability and External Services
Sentry for Error Tracking
Production deployments use Sentry (@sentry/node, @sentry/profiling-node) to capture unhandled exceptions and performance profiles. Integration is initialized early in the application bootstrap.
AWS S3 for File Storage
Task attachments and other assets upload directly to S3-compatible storage using the AWS SDK (@aws-sdk/client-s3, @aws-sdk/s3-request-presigner). The backend generates presigned URLs rather than proxying file content.
Additional Integrations
The apps/api/package.json includes specialized libraries for specific features:
| Library | Purpose |
|---|---|
| Octokit | GitHub and Gitea API integration |
| @modelcontextprotocol/sdk | Model Context Protocol support |
| croner | Scheduled job execution |
| creem | Payment processing |
| @oslojs/crypto | Additional cryptographic utilities |
Testing with Vitest
The backend test suite runs on Vitest, covering:
- Unit tests for utility functions and schema validation
- Integration tests for API routes with in-memory PostgreSQL
- WebSocket broadcast logic verification
Tests co-locate with source files or reside in __tests__ directories, following the project's convention.
File-by-File Stack Reference
| File | Responsibility |
|---|---|
apps/api/package.json |
Dependency declarations and engine requirements |
apps/api/src/index.ts |
Hono server bootstrap, middleware chain, WebSocket setup |
apps/api/src/database/index.ts |
PostgreSQL pool, Drizzle instance, lazy DB proxy |
apps/api/src/database/schema.ts |
Table definitions with Drizzle ORM |
apps/api/src/ws/in-memory-broadcast-adapter.ts |
Single-instance WebSocket broadcasting |
apps/api/src/ws/redis-broadcast-adapter.ts |
Distributed WebSocket broadcasting via Redis |
Summary
- Kaneo's backend technology stack centers on Node.js 20, TypeScript, and Hono for a lightweight, type-safe HTTP API
- Drizzle ORM with PostgreSQL provides strongly-typed database access without ORM overhead
- @hono/zod-openapi generates OpenAPI specs and validates requests at runtime
- better-auth handles sessions, API keys, and OAuth in a modular package
- WebSocket real-time updates scale from in-memory (single instance) to Redis-backed (multi-instance)
- Sentry, AWS S3 SDK, and targeted libraries complete the production-ready architecture
This stack prioritizes developer experience (TypeScript everywhere, fast builds), operational simplicity (single Docker image possible), and scalability (Redis, connection pooling, optional service separation).
Frequently Asked Questions
What web framework does Kaneo use for its backend API?
Kaneo uses Hono, a lightweight TypeScript-first web framework. Hono handles routing, middleware (CORS, compression), OpenAPI documentation, and WebSocket upgrades through the @hono/node-server and @hono/node-ws packages. The framework choice keeps bundle sizes small while maintaining full type inference across the application.
How does Kaneo validate API requests?
Kaneo validates requests using Zod schemas integrated through @hono/zod-openapi. Route handlers receive validated, typed parameters via c.req.valid()—no manual parsing needed. This same schema automatically generates OpenAPI documentation and TypeScript types, ensuring the API contract stays synchronized across code and docs.
Can Kaneo scale beyond a single server instance?
Yes. The WebSocket layer supports both in-memory broadcasting (default, single instance) and Redis-backed broadcasting (multi-instance). When REDIS_URL is configured, Kaneo uses ioredis to distribute events across all connected API instances. The database layer uses pg connection pooling, and the stateless HTTP design allows horizontal scaling behind any load balancer.
What database does Kaneo require?
Kaneo requires PostgreSQL as its primary datastore. The backend connects via the pg driver and queries through Drizzle ORM, a type-safe SQL-like query builder. Schema migrations run via drizzle-kit. No other database engines are currently supported in the core backend implementation.
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 →