# Kaneo Backend Technology Stack: Node.js, TypeScript, Hono, and Drizzle ORM Explained

> Discover the Kaneo backend technology stack: Node.js, TypeScript, Hono, and Drizzle ORM. Learn how these tools power efficient and scalable applications. Get the details now!

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: internals
- Published: 2026-08-29

---

**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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/package.json), the configuration explicitly declares the module type:

```json
{
  "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-ws` for real-time features

### Server Bootstrap in index.ts

The main application entry point in [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts) demonstrates the complete Hono setup:

```typescript
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:

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/index.ts), the database layer initializes:

```typescript
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

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/in-memory-broadcast-adapter.ts) maintains a local Map of connected clients:

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/redis-broadcast-adapter.ts) uses **ioredis** to subscribe to a Redis channel and forward messages to local WebSocket clients:

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/package.json) | Dependency declarations and engine requirements |
| [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts) | Hono server bootstrap, middleware chain, WebSocket setup |
| [`apps/api/src/database/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/index.ts) | PostgreSQL pool, Drizzle instance, lazy DB proxy |
| [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) | Table definitions with Drizzle ORM |
| [`apps/api/src/ws/in-memory-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/in-memory-broadcast-adapter.ts) | Single-instance WebSocket broadcasting |
| [`apps/api/src/ws/redis-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/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.