AFFiNE Backend Architecture: How the TypeScript and Rust Codebase is Structured

The AFFiNE backend combines a NestJS TypeScript API server with a high-performance Rust storage engine, using NAPI-RS bindings to bridge the two for local-first collaborative editing.

The open-source AFFiNE repository (toeverything/AFFiNE) implements a modular backend architecture that splits responsibilities between a Node.js application server and a native Rust binary. This structure enables high-performance CRDT operations while maintaining a flexible TypeScript API layer for HTTP and GraphQL endpoints.

Overview of the AFFiNE Backend Stack

According to the toeverything/AFFiNE source code, the backend is organized as two primary Yarn workspaces defined in the root tsconfig.json:

  • packages/backend/server: The TypeScript application server built on NestJS
  • packages/backend/server-native: The Rust storage module compiled via Cargo and exposed to Node via napi-rs

These workspaces share a unified build system. The root tsconfig.json references both paths to enable cross-package development:

{
  "references": [
    { "path": "./packages/backend/server" },
    { "path": "./packages/backend/server-native" }
  ]
}

Source: [tsconfig.json](https://github.com/toeverything/AFFiNE/blob/canary/tsconfig.json)

The TypeScript Layer: @affine/server

The @affine/server workspace implements the public API surface, authentication, job scheduling, and real-time communication layers.

NestJS Framework and Core Infrastructure

The server bootstraps from packages/backend/server/src/main.ts and defines its module structure in packages/backend/server/src/app.module.ts. This module imports GraphQL, WebSocket gateways, and BullMQ queues required for asynchronous processing.

Key technologies include:

  • NestJS (core, platform-express, websockets) for the HTTP server and dependency injection
  • Apollo Server for GraphQL query handling
  • TRPC and Hono for additional API routing options
  • Swagger for API documentation

Background Job Processing with BullMQ

Long-running tasks execute outside the request cycle through BullMQ processors. For example, email notifications run via a dedicated job processor:

// packages/backend/server/src/jobs/email.job.ts
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bullmq';

@Processor('email')
export class EmailProcessor {
  @Process()
  async handle(job: Job<{ to: string; subject: string; body: string }>) {
    // call external mail service
    await sendMail(job.data);
  }
}

Source: [email.job.ts](https://github.com/toeverything/AFFiNE/blob/canary/packages/backend/server/src/jobs/email.job.ts)

The Rust Layer: @affine/server-native

The @affine/server-native workspace provides the persistence layer through a Rust binary. It handles CRDT state management and local-first storage via OctoBase and y-octo.

OctoBase Storage and y-octo CRDTs

OctoBase is an embedded, multi-tenant storage engine that uses SQLite under the hood. It integrates y-octo (a native Rust implementation of Yjs CRDTs) to manage document state and synchronization. The Rust crate is defined in the root Cargo.toml:

[package]
name = "affine"
version = "0.12.0"
edition = "2021"

[dependencies]
anyhow = "1"
clap = { version = "4.4", features = ["derive"] }
env_logger = "0.11"
log = "0.4"
octobase = { version = "0.6", features = ["fs"] }
y-octo = "0.2"

Source: [Cargo.toml](https://github.com/toeverything/AFFiNE/blob/canary/Cargo.toml)

NAPI-RS Bindings

The Rust functions are exposed to the TypeScript runtime through NAPI-RS bindings. The TypeScript side of this bridge in packages/backend/server-native/src/index.ts exports functions that internally call the compiled binary:

// packages/backend/server-native/src/index.ts
import { OctoBase } from 'octobase';
import { Yocto } from 'y-octo';

export async function initDB(path: string) {
  const db = await OctoBase.open(path);
  const crdt = new Yocto(db);
  return { db, crdt };
}

export async function ping() {
  // lightweight health check implemented in Rust
  return 'online';
}

Source: [index.ts](https://github.com/toeverything/AFFiNE/blob/canary/packages/backend/server-native/src/index.ts)

Integration: Bridging TypeScript and Rust

The TypeScript server invokes native operations through the @affine/server-native package. For example, a health check controller calls the Rust ping implementation directly:

// packages/backend/server/src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ServerNative } from '@affine/server-native';

@Controller('health')
export class HealthController {
  @Get()
  async check() {
    const status = await ServerNative.ping(); // NAPI call to Rust side
    return { ok: true, storage: status };
  }
}

Source: [app.controller.ts](https://github.com/toeverything/AFFiNE/blob/canary/packages/backend/server/src/app.controller.ts)

Development Workflow

The CLI script at tools/cli/src/run.ts detects the target workspace and loads the appropriate runtime. Use the following commands during development:

  • Start the TypeScript server: yarn affine dev -p @affine/server
  • Build the native module: yarn workspace @affine/server-native run build (invokes cargo build)

Source: [run.ts](https://github.com/toeverything/AFFiNE/blob/canary/tools/cli/src/run.ts)

Summary

  • The AFFiNE backend uses a hybrid TypeScript/Rust architecture where Node.js handles the API and Rust manages storage.
  • @affine/server provides the NestJS HTTP server, GraphQL endpoints, WebSocket real-time sync, and BullMQ job processing.
  • @affine/server-native delivers high-performance CRDT operations via OctoBase and y-octo, compiled as a Rust binary with NAPI-RS bindings.
  • Configuration files tsconfig.json and Cargo.toml orchestrate the multi-language workspace structure.
  • Integration happens through NAPI-RS calls, allowing TypeScript controllers to execute Rust-native storage operations seamlessly.

Frequently Asked Questions

What is the primary framework used for the AFFiNE backend API?

The primary framework is NestJS, which provides the HTTP server, dependency injection, GraphQL integration via Apollo, and WebSocket support. It is implemented in the packages/backend/server workspace.

How does AFFiNE handle data persistence and synchronization?

Data persistence is handled by the OctoBase storage engine written in Rust, which uses SQLite for local storage and y-octo for CRDT-based synchronization. This enables local-first editing where changes sync across clients in real time.

What is the role of Rust in the AFFiNE backend architecture?

Rust provides the native storage module (@affine/server-native) that compiles to a binary and exposes high-performance database and CRDT functions to Node.js via NAPI-RS bindings. This avoids JavaScript's performance limitations for intensive data operations.

How are background jobs managed in AFFiNE?

Background jobs use BullMQ, a Redis-backed queue system for Node.js. Processes like email sending run in separate workers defined in the jobs directory of the server package, while the main NestJS application enqueues tasks via the @nestjs/bull decorators.

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 →