Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

AFFiNE Administrator & Developer Guide

Generated May 2026 from toeverything/AFFiNE at df482c9

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

1. Architecture Overview

AFFiNE is a privacy-focused, local-first knowledge workspace built as a TypeScript + Rust monorepo. It combines a block-based document editor, an infinite canvas whiteboard, and a structured database system into a single application available on web, desktop (Electron), and mobile (Android/iOS) platforms.

System Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Client Applications                      │
│  ┌──────────┐  ┌──────────────┐  ┌─────────┐  ┌──────────┐ │
│  │ Web App  │  │ Desktop App  │  │ Android │  │   iOS    │ │
│  │ (React)  │  │ (Electron)   │  │(Capacitor│  │ (Swift)  │ │
│  └────┬─────┘  └──────┬───────┘  └────┬────┘  └────┬─────┘ │
│       └────────────────┴───────────────┴────────────┘       │
│                         │                                     │
│              ┌──────────┴──────────┐                         │
│              │     BlockSuite      │                         │
│              │  (Editor Framework) │                         │
│              │  Blocks │ Widgets   │                         │
│              │  GFX    │ Inlines   │                         │
│              └──────────┬──────────┘                         │
│                         │                                     │
│              ┌──────────┴──────────┐                         │
│              │     y-octo (CRDT)   │                         │
│              │   (Rust via NAPI)   │                         │
│              └──────────┬──────────┘                         │
└─────────────────────────┼───────────────────────────────────┘
                          │ GraphQL / WebSocket
┌─────────────────────────┼───────────────────────────────────┐
│              AFFiNE Server (NestJS)                          │
│  ┌──────────────────────┴──────────────────────────────┐    │
│  │                   Server Flavors                     │    │
│  │  graphql │ sync │ renderer │ front │ doc │ allinone  │    │
│  └──────────────────────┬──────────────────────────────┘    │
│              ┌──────────┴──────────┐                         │
│  ┌───────┐  │      Core Modules    │  ┌─────────────────┐  │
│  │ Redis │◄─┤ Auth │ Permission    │  │    Plugins       │  │
│  │(Cache)│  │ Sync │ Storage       │  │ Copilot │ OAuth  │  │
│  └───────┘  │ Quota│ Notification  │  │ Payment │ License│  │
│              └──────────┬──────────┘  │ Indexer │ Worker │  │
│  ┌───────────┐         │              └─────────────────┘  │
│  │PostgreSQL │◄────────┘                                    │
│  │ (Prisma)  │                                              │
│  └───────────┘                                              │
└─────────────────────────────────────────────────────────────┘

Component Inventory

Component Path Role
Frontend Core packages/frontend/core/ Main application shell, modules (cloud, auth, editor, collections, search, AI)
Web App packages/frontend/apps/web/ Web SPA entry point
Desktop App packages/frontend/apps/electron/ Electron main process, renderer
Android App packages/frontend/apps/android/ Capacitor + native Java plugins
iOS App packages/frontend/apps/ios/ Native Swift plugins
Mobile Shared packages/frontend/apps/mobile-shared/ Shared mobile code
Admin Panel packages/frontend/admin/ Server administration UI
Backend Server packages/backend/server/ NestJS API server
Backend Native packages/backend/native/ Rust native modules for server
Frontend Native packages/frontend/native/ Rust NAPI bindings (SQLite, media, schema)
BlockSuite blocksuite/ Block editor framework (blocks, widgets, GFX, inlines, fragments)
Common Infra packages/common/infra/ DI framework, ORM, utilities
Common GraphQL packages/common/graphql/ Generated GraphQL types and queries
y-octo packages/common/y-octo/ Rust CRDT implementation (Yjs compatible)
Reader packages/common/reader/ Document parser (delta-to-markdown, DOCX)
S3 Compat packages/common/s3-compat/ S3-compatible storage adapter
i18n packages/frontend/i18n/ Internationalization (30+ languages)
CLI Tools tools/cli/ Build tooling, bundling, dev server
Route System packages/frontend/routes/ Frontend routing
Analytics/Tracking packages/frontend/track/ Event tracking

Technology Stack

Layer Technology
Frontend framework React 18 + Lit (Web Components for BlockSuite)
State management @preact/signals-core (reactive signals)
Editor framework BlockSuite (custom, Lit-based)
CRDT sync y-octo (Rust) / Yjs
Desktop runtime Electron 39
Mobile framework Capacitor 7 (Android), Swift (iOS)
Backend framework NestJS (Node.js)
API protocol GraphQL (via @nestjs/graphql)
Real-time sync WebSocket
Database PostgreSQL (via Prisma ORM)
Caching/Queue Redis
Native modules Rust via NAPI-RS
Package manager Yarn 4.13.0 (workspaces)
Build tool Vite 7
Testing Vitest (unit), Playwright 1.58 (E2E), AVA (backend)
Linting ESLint 9 + oxlint 1.58 + Prettier
Language TypeScript 5.9, Rust (2024 edition)

Note

AFFiNE uses a Dependency Injection framework (packages/common/infra/src/framework/core/) for frontend service management, modeled after Angular/NestJS patterns. All frontend modules register services through this DI system.

Data Flow

Document editing:

  1. User types in BlockSuite editor → block model updates via Yjs document
  2. y-octo (Rust CRDT) processes the update locally
  3. Update is persisted to local SQLite (desktop) or IndexedDB (web)
  4. If cloud sync is enabled, update is sent via WebSocket to the sync server
  5. Server merges CRDT updates and broadcasts to other connected clients

Server API requests:

  1. Client sends GraphQL query/mutation via HTTPS
  2. NestJS resolves the request through configured modules
  3. Prisma executes database operations against PostgreSQL
  4. Response returns to client

2. System Requirements

Server Requirements

Requirement Minimum Recommended
Node.js 18.x 20.x–22.x (must be <23.0.0)
PostgreSQL 14 16+
Redis 6 7+
RAM 2 GB 4+ GB
Disk 10 GB 50+ GB (scales with blob storage)
OS Linux (x64/arm64) Ubuntu 22.04+

Development Requirements

Requirement Version
Node.js <23.0.0
Yarn 4.13.0 (via corepack)
Rust Latest stable (2024 edition)
Python 3.x (for node-gyp native builds)
Docker 20+ (optional, for containerized development)

Important

The monorepo uses Yarn 4 Berry with PnP. Do not use npm or pnpm. The packageManager field in package.json enforces yarn@4.13.0.


3. Configuration Reference

Server Configuration System

AFFiNE uses a layered configuration system with compile-time defaults, environment variables, JSON config files, and runtime database overrides.

Configuration precedence (highest to lowest):

  1. Database-stored config (set via GraphQL updateAppConfig mutation)
  2. JSON config file (config.json in project root or ~/.affine/config/config.json)
  3. Environment variables
  4. Code defaults (defined via defineModuleConfig())

Important

Configuration is validated at startup using Zod schemas. Invalid values cause the server to fail fast with descriptive error messages.

Server Module Config

Defined in packages/backend/server/src/core/config/config.ts:

Key Env Variable Type Default Description
server.name — string undefined Recognizable name for the server, shown in AFFiNE Desktop
server.externalUrl AFFINE_SERVER_EXTERNAL_URL string "" Base URL for generating external URLs
server.https AFFINE_SERVER_HTTPS boolean false Whether server uses HTTPS
server.host AFFINE_SERVER_HOST string localhost FQDN where server is deployed
server.hosts — string[] [] Multiple hosts the server will accept requests from
server.listenAddr LISTEN_ADDR string 0.0.0.0 Address to bind to (e.g., :: for IPv6)
server.port AFFINE_SERVER_PORT integer 3010 Port to listen on
server.path AFFINE_SERVER_SUB_PATH string "" URL subpath (e.g., /affine)
flags.allowGuestDemoWorkspace — boolean true Allow guest users to create demo workspaces

Environment Variables

Variable Purpose Required
NODE_ENV Environment: development, test, production No (default: production)
AFFINE_ENV Namespace: dev, beta, production No (default: production)
DEPLOYMENT_TYPE affine or selfhosted No (auto-detected)
SERVER_FLAVOR Server flavor (see below) No (default: allinone)
DEPLOYMENT_PLATFORM gcp or unknown No (default: unknown)
AFFINE_PRIVATE_KEY Auth signing private key Yes (for auth)
AFFINE_SERVER_EXTERNAL_URL Public-facing server URL Recommended
AFFINE_SERVER_HTTPS Enable HTTPS (true/false) No
AFFINE_SERVER_HOST Server hostname No (default: localhost)
AFFINE_SERVER_PORT Server port No (default: 3010)
LISTEN_ADDR Bind address No (default: 0.0.0.0)
AFFINE_SERVER_SUB_PATH URL subpath No
CAPTCHA_SITE_KEY CAPTCHA provider site key No
SENTRY_DSN Sentry error tracking DSN No
CHANGELOG_URL URL for changelog feed No
SELF_HOSTED Flag for self-hosted builds No
R2_SECRET_ACCESS_KEY Cloudflare R2 storage key No (cloud only)
GITHUB_TOKEN GitHub API token (for changelogs) No

Server Flavors

The server supports multiple deployment flavors for microservice or monolithic architectures:

Flavor Value Description
All-in-One allinone All services in a single process (default, recommended for self-hosting)
GraphQL graphql GraphQL API server only
Sync sync WebSocket sync server only
Renderer renderer Document rendering service
Front front Static frontend + sync + doc services
Doc doc Document storage service
Script script Script execution mode (explicit only)

Note

In allinone mode, the server loads ALL modules (GraphQL, sync, renderer, doc, worker). For high-scale deployments, split into separate processes using individual flavors.

Runtime Configuration via GraphQL

Administrators can update configuration at runtime through the GraphQL API:

mutation {
  updateAppConfig(updates: [
    { module: "server", key: "name", value: "My AFFiNE Server" }
  ])
}
query {
  appConfig
}

Configuration validation is available:

query {
  validateAppConfig(updates: [
    { module: "server", key: "port", value: 8080 }
  ]) {
    module
    key
    valid
    error
  }
}

Warning

Only users with the Admin role can access appConfig queries and mutations. Invalid config values are rejected with descriptive validation errors.

JSON Config File

Place a config.json in the project root or ~/.affine/config/config.json:

{
  "$schema": "./node_modules/@affine/server/config-schema.json",
  "server": {
    "name": "My AFFiNE Instance",
    "externalUrl": "https://affine.example.com",
    "https": true,
    "host": "affine.example.com",
    "port": 3010
  }
}

CLI Commands

The server includes a CLI for data migration management:

# Run the CLI
node --loader ts-node/esm packages/backend/server/src/cli.ts <command>
Command Description
create [name] Create a new data migration script
run Run all pending data migrations
revert [name] Revert a specific data migration
import-config [path] Import configuration from a JSON file
# Run all pending migrations
yarn affine run

# Create a new migration
yarn affine create add-user-preferences

# Import config from file
yarn affine import-config ./my-config.json

Tip

Data migrations are timestamp-ordered and tracked in the DataMigration table. Each migration has up and down methods for forward and rollback operations.


4. Deployment & Scaling

Docker Deployment (Self-Hosted)

The recommended self-hosting method uses Docker:

docker pull ghcr.io/toeverything/affine-graphql:stable

Required services:

Service Purpose
affine-graphql Main AFFiNE server (all-in-one)
PostgreSQL Primary database
Redis Caching, job queue, pub/sub

Minimal docker-compose example:

version: '3.8'
services:
  affine:
    image: ghcr.io/toeverything/affine-graphql:stable
    ports:
      - "3010:3010"
    environment:
      NODE_ENV: production
      AFFINE_SERVER_HOST: affine.example.com
      AFFINE_SERVER_HTTPS: "true"
      AFFINE_SERVER_EXTERNAL_URL: https://affine.example.com
      AFFINE_PRIVATE_KEY: "${AFFINE_PRIVATE_KEY}"
      DATABASE_URL: postgresql://affine:password@postgres:5432/affine
      REDIS_SERVER_HOST: redis
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: affine
      POSTGRES_PASSWORD: password
      POSTGRES_DB: affine
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Caution

Set a strong, random AFFINE_PRIVATE_KEY in production. This key signs authentication tokens — compromising it allows forging user sessions.

Scaling Strategies

Horizontal scaling with flavors:

For high-traffic deployments, split the server into microservices using flavors:

# GraphQL API
affine-api:
  image: ghcr.io/toeverything/affine-graphql:stable
  environment:
    SERVER_FLAVOR: graphql

# Sync server (WebSocket)
affine-sync:
  image: ghcr.io/toeverything/affine-graphql:stable
  environment:
    SERVER_FLAVOR: sync

# Document service
affine-doc:
  image: ghcr.io/toeverything/affine-graphql:stable
  environment:
    SERVER_FLAVOR: doc

Use a reverse proxy (Nginx, Traefik) to route traffic:

  • /graphql → affine-api
  • /socket.io → affine-sync
  • /* → affine-front (if using front flavor)

Reverse Proxy Configuration

Nginx example:

server {
    listen 443 ssl;
    server_name affine.example.com;

    location / {
        proxy_pass http://localhost:3010;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /socket.io {
        proxy_pass http://localhost:3010;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Important

WebSocket support is required for real-time sync. Ensure your reverse proxy passes the Upgrade and Connection headers for the /socket.io path.


5. Operations

Health Monitoring

The server exposes an info endpoint at GET /info:

curl https://affine.example.com/info
{
  "compatibility": "0.26.3",
  "message": "AFFiNE 0.26.3 Server",
  "type": "selfhosted",
  "flavor": "allinone"
}

Tip

Use the /info endpoint as a health check target in your load balancer or monitoring system. It responds without authentication.

Server Features Query

Check enabled features via GraphQL:

query {
  serverConfig {
    name
    version
    baseUrl
    type
    features
    initialized
  }
}

The initialized field returns true once at least one user account exists.

Upgrade Checking

Self-hosted servers automatically check for available upgrades via the availableUpgrade resolver:

query {
  serverConfig {
    availableUpgrade {
      version
      url
      changelog
      publishedAt
    }
  }
}

This queries https://affine.pro/api/worker/releases for the latest release in the configured channel (stable, beta, or canary based on AFFINE_ENV).

Note

Upgrade checking only activates for self-hosted deployments (DEPLOYMENT_TYPE=selfhosted). Cloud instances return null.

Database Migrations

Run data migrations after upgrades:

# Inside the container
node --loader ts-node/esm src/cli.ts run

Migrations are idempotent — re-running run skips already-applied migrations. Some migrations are marked always: true and re-execute on every run.

Backup Strategy

Component Backup Method
PostgreSQL pg_dump affine > backup.sql
Redis RDB snapshots or AOF persistence
Blob storage Back up the configured storage directory or S3 bucket
Config Export via appConfig GraphQL query

Log Management

The server uses NestJS's built-in logger with structured output. Configure log levels via environment:

NODE_ENV=production  # Minimal logging
NODE_ENV=development # Verbose logging

6. Performance

Frontend Performance

  • y-octo (Rust CRDT): Native Rust CRDT operations via NAPI-RS provide significantly faster document merge performance compared to pure JavaScript Yjs
  • Turbo Renderer: The edgeless canvas uses a GPU-accelerated turbo renderer (blocksuite/affine/gfx/turbo-renderer/) for large whiteboards
  • Lazy loading: BlockSuite extensions are loaded on demand via the ext-loader system (blocksuite/affine/ext-loader/)
  • SQLite (desktop): Desktop app uses native SQLite (via packages/frontend/native/sqlite_v1/) for local document storage, providing faster reads than IndexedDB

Backend Performance

  • Request ID tracing: Every HTTP/GraphQL request gets a unique ID via nestjs-cls for end-to-end tracing
  • Rate limiting: Built-in throttler (RateLimiterModule) prevents abuse. Rate limits can be skipped for specific endpoints using the @SkipThrottle() decorator
  • Redis caching: Frequently accessed data is cached in Redis via CacheModule
  • Prisma query optimization: The @nestjs-cls/transactional adapter wraps database operations in transactions with automatic rollback

Tip

For large workspaces (1000+ documents), ensure Redis has sufficient memory for sync state caching. Monitor PostgreSQL query performance and add indexes for custom queries.


7. Security

Authentication

AFFiNE supports multiple authentication methods:

Method Module Description
Email/Password AuthModule Built-in credential auth with configurable password requirements
OAuth/OIDC OAuthModule External identity providers (Google, GitHub, custom OIDC)
CAPTCHA CaptchaModule Bot protection for sign-up and sign-in

Password requirements are configurable via the auth.passwordRequirements config:

  • min: Minimum password length
  • max: Maximum password length

Authorization

The permission system uses role-based access control (RBAC) with two resource types:

Workspace roles:

Role Capabilities
Owner Full control, including workspace deletion
Admin Manage members, settings, features
Editor Create and edit documents
Viewer Read-only access

Document roles:

Role Capabilities
Owner Full document control, sharing, deletion
Editor Edit document content
Viewer Read-only access

The AccessControllerBuilder (packages/backend/server/src/core/permission/builder.ts) provides a fluent API for permission checks:

const canEdit = await accessController
  .user(userId)
  .doc(workspaceId, docId)
  .can('Doc.Write');

Workspace policies:

  • Workspaces become read-only when member count or storage exceeds quota
  • Read-only reasons: member_overflow, storage_overflow
  • Team workspaces have separate quota tiers

Caution

Owner role cannot be granted via the API (Cannot grant Owner role of a workspace to a user). Ownership transfer requires special procedures.

API Security

  • Rate limiting: All API endpoints are rate-limited by default. Use @SkipThrottle() only for health/info endpoints
  • Request tracing: Every request gets a unique ID (X-Request-Id header) for audit logging
  • CORS: Configured per deployment via server.hosts (allowed origins)
  • GraphQL introspection: Should be disabled in production deployments

Secrets Management

Secret Purpose Storage
AFFINE_PRIVATE_KEY JWT token signing Environment variable
Database password PostgreSQL auth Environment variable / DATABASE_URL
Redis password Redis auth Environment variable
OAuth client secrets OIDC provider auth Database (encrypted via config system)
Copilot API keys LLM provider auth Database (via config system)

Warning

Never commit secrets to version control. Use environment variables or a secrets manager. The AFFINE_PRIVATE_KEY must be consistent across all server instances in a scaled deployment.


8. Developer Internals

Repository Structure

toeverything/AFFiNE/
├── blocksuite/                  # Block editor framework
│   ├── affine/                  # AFFiNE-specific extensions
│   │   ├── blocks/              # Block types (paragraph, list, code, image, etc.)
│   │   ├── widgets/             # Editor widgets (toolbar, slash-menu, drag-handle)
│   │   ├── gfx/                 # Graphics elements (shape, connector, text, brush)
│   │   ├── inlines/             # Inline elements (link, mention, latex, footnote)
│   │   ├── fragments/           # UI fragments (outline, frame-panel, doc-title)
│   │   ├── shared/              # Shared utilities, services, adapters
│   │   ├── model/               # Block model definitions
│   │   ├── components/          # Reusable UI components
│   │   ├── data-view/           # Database view framework
│   │   ├── rich-text/           # Rich text editing
│   │   ├── foundation/          # Foundation extensions
│   │   ├── ext-loader/          # Extension loading system
│   │   └── all/                 # Barrel package (@blocksuite/affine)
│   ├── framework/               # Core framework
│   │   ├── store/               # Document store, schema, adapters
│   │   ├── std/                 # Standard library (events, inline editing)
│   │   ├── global/              # Global types and utilities
│   │   └── sync/                # Sync utilities
│   ├── playground/              # Development playground
│   └── integration-test/        # Integration tests
├── packages/
│   ├── backend/
│   │   ├── server/              # NestJS server application
│   │   │   └── src/
│   │   │       ├── app.module.ts    # Module composition
│   │   │       ├── cli.ts           # CLI entry point
│   │   │       ├── env.ts           # Environment configuration
│   │   │       ├── base/            # Base infrastructure
│   │   │       │   ├── config/      # Configuration system
│   │   │       │   ├── error/       # Error handling (233 error types)
│   │   │       │   ├── event/       # Event bus
│   │   │       │   └── ...          # Cache, Redis, storage, etc.
│   │   │       ├── core/            # Core business logic
│   │   │       │   ├── auth/        # Authentication
│   │   │       │   ├── permission/  # RBAC authorization
│   │   │       │   ├── config/      # Server config resolver
│   │   │       │   ├── sync/        # Real-time sync
│   │   │       │   ├── doc/         # Document storage
│   │   │       │   ├── quota/       # Usage quotas
│   │   │       │   └── ...
│   │   │       ├── plugins/         # Optional plugins
│   │   │       │   ├── copilot/     # AI copilot (multi-provider)
│   │   │       │   ├── oauth/       # OAuth/OIDC
│   │   │       │   ├── payment/     # Stripe payments
│   │   │       │   ├── license/     # License management
│   │   │       │   ├── indexer/     # Full-text search indexer
│   │   │       │   └── ...
│   │   │       ├── models/          # Data models (Prisma)
│   │   │       └── data/            # Migrations
│   │   └── native/              # Rust native modules (server-side)
│   ├── frontend/
│   │   ├── core/                # Main application code
│   │   │   └── src/modules/     # Feature modules (50+ modules)
│   │   ├── apps/
│   │   │   ├── web/             # Web SPA entry
│   │   │   ├── electron/        # Electron app
│   │   │   ├── android/         # Android (Capacitor)
│   │   │   ├── ios/             # iOS (Swift)
│   │   │   └── mobile/          # Mobile web
│   │   ├── admin/               # Admin panel
│   │   ├── native/              # Rust NAPI modules
│   │   │   ├── nbstore/         # Native blob/doc store
│   │   │   ├── sqlite_v1/       # SQLite bindings
│   │   │   ├── schema/          # Schema validation
│   │   │   └── media_capture/   # Audio/video capture
│   │   ├── component/           # Shared UI components
│   │   ├── i18n/                # Internationalization
│   │   ├── routes/              # Frontend routing
│   │   └── track/               # Analytics
│   └── common/
│       ├── infra/               # DI framework, ORM, utilities
│       ├── graphql/             # Generated GraphQL client
│       ├── native/              # Shared Rust modules
│       ├── y-octo/              # CRDT implementation
│       ├── reader/              # Document parsers
│       ├── s3-compat/           # S3 storage adapter
│       ├── error/               # Error types
│       ├── debug/               # Debug utilities
│       └── nbstore/             # Notebook store interface
├── tools/
│   ├── cli/                     # Build and dev CLI
│   ├── utils/                   # Build config utilities
│   ├── doc-diff/                # Documentation diff tool
│   ├── commitlint/              # Commit message linting
│   └── playstore-auto-bump/     # Play Store version bumper
├── tests/
│   ├── affine-local/            # Local-mode E2E tests
│   ├── affine-cloud/            # Cloud E2E tests
│   ├── affine-desktop/          # Desktop E2E tests
│   ├── affine-desktop-cloud/    # Desktop + cloud E2E tests
│   ├── affine-mobile/           # Mobile E2E tests
│   ├── affine-cloud-copilot/    # AI copilot E2E tests
│   ├── blocksuite/              # BlockSuite E2E tests
│   └── kit/                     # Test utilities and helpers
├── docs/
│   ├── contributing/            # Contributing guides
│   └── reference/               # API reference docs
├── scripts/                     # Setup scripts
├── package.json                 # Root workspace config
├── Cargo.toml                   # Rust workspace config
└── .oxlintrc.json               # Linting configuration

BlockSuite Architecture

BlockSuite is AFFiNE's custom block-based editor framework, built on Lit Web Components. It provides:

Core concepts:

Concept Description
Block Fundamental content unit (paragraph, list, image, etc.)
Store Document state container backed by Yjs
Schema Type definitions for block models
Command Composable editor operations (chainable via .pipe())
Extension Plugin system (StoreExtensionProvider, ViewExtensionProvider)
Widget Floating UI elements (toolbar, slash menu, drag handle)
GFX Graphics framework for edgeless mode (shapes, connectors)
Inline Inline formatting and elements (bold, link, mention)

Extension pattern:

Every block type follows a consistent pattern:

// Store extension — registers the block model
export class MyBlockStoreExtension extends StoreExtensionProvider { ... }

// View extension — registers the block component
export class MyBlockViewExtension extends ViewExtensionProvider { ... }

Block flavors registered in the system:

  • affine:paragraph, affine:list, affine:code, affine:image
  • affine:bookmark, affine:attachment, affine:callout
  • affine:divider, affine:latex, affine:embed
  • affine:database, affine:data-view, affine:table
  • affine:surface, affine:surface-ref, affine:note
  • affine:frame, affine:edgeless-text

Frontend Module System

The frontend uses a DI-based module system in packages/frontend/core/src/modules/. Each module follows this pattern:

modules/<feature>/
├── services/          # Business logic services (extend Service class)
├── views/             # React components
├── utils/             # Helper functions
└── index.ts           # Module registration

Key frontend modules (50+):

Module Purpose
cloud/ Cloud sync, auth, workspace management (27 service files)
editor-setting/ Editor preferences
permissions/ Permission checks and member management
collection/ Collection management
collection-rules/ Smart collection filter rules
docs-search/ Full-text document search
doc/ Document lifecycle
doc-summary/ AI-generated document summaries
workbench/ Multi-tab workspace UI
workspace/ Workspace management
ai-button/ AI copilot button and UI
media/ Audio/video recording and transcription
dnd/ Drag-and-drop system
find-in-page/ Ctrl+F find functionality
journal/ Daily journal entries
notification/ User notifications
quicksearch/ Quick search (Ctrl+K)
favorite/ Favorite pages
import-template/ Template importing
import-clipper/ Web clipper import
peek-view/ Peek preview of linked docs
navigation/ Page navigation
theme-editor/ Theme customization
pdf/ PDF rendering
at-menu-config/ @-mention menu configuration

Backend Module Architecture

The NestJS server (packages/backend/server/) uses a modular architecture with conditional loading:

// From app.module.ts — modules loaded based on server flavor
export function buildAppModule(env: Env) {
  const factor = new AppModuleBuilder();
  factor
    .use(...FunctionalityModules)  // Always loaded
    .use(UserModule, AuthModule, PermissionModule)  // Auth
    .useIf(() => env.flavors.graphql, GqlModule, ...)  // GraphQL only
    .useIf(() => env.flavors.sync, SyncModule, ...)  // Sync only
    .useIf(() => env.dev || env.selfhosted, WorkerModule, SelfhostModule)
  return factor.compile();
}

Always-loaded modules (FunctionalityModules):

Module Purpose
ClsModule Request-scoped context (unique request IDs)
LoggerModule Structured logging
PrismaModule Database access
EventModule Async event bus
ConfigModule Configuration management
RedisModule Redis client
CacheModule Caching layer
MutexModule Distributed locks
MetricsModule Performance metrics
RateLimiterModule API rate limiting
StorageProviderModule Storage abstraction
WebSocketModule WebSocket support
JobModule Background job processing
ModelsModule Data model layer
MonitorModule Health monitoring

Copilot (AI) Provider System

The copilot plugin (packages/backend/server/src/plugins/copilot/providers/) supports multiple LLM backends:

Provider Type Implementation Capabilities
OpenAI OpenAI API Text, image, embeddings
Google Gemini Google AI API Text, image, audio, file
Cloudflare Workers AI Workers AI API Text, reranking
Native (llm_adapter) Rust native adapter Text streaming, reranking

Providers are registered via CopilotProviderFactory which resolves models based on capability requirements (input types, output types). Configuration is stored in config.copilot.providers.

ORM Layer

The frontend uses a custom ORM (packages/common/infra/src/orm/core/) for client-side data:

// Define a table schema
const UserSettingsTable = new Table('user_settings', {
  id: f.string().primaryKey(),
  theme: f.string().default('system'),
  language: f.string().default('en'),
});

Adapters: The ORM supports pluggable storage adapters (packages/common/infra/src/orm/core/adapters/).

Error Handling

The backend defines a comprehensive error hierarchy in packages/backend/server/src/base/error/:

  • UserFriendlyError — base class for errors shown to users
  • GraphQLError — GraphQL-specific errors
  • Domain-specific errors: DocActionDenied, SpaceAccessDenied, OwnerCanNotLeaveWorkspace, InvalidAppConfigInput, etc.

The system includes 233 error types covering authentication, authorization, quota, sync, and configuration failures.

Testing Strategy

Test Type Framework Location Command
Unit tests Vitest packages/*/src/__tests__/ yarn test
Backend tests AVA packages/backend/server/src/__tests__/ yarn test
E2E (web) Playwright tests/affine-local/, tests/affine-cloud/ yarn playwright test
E2E (desktop) Playwright tests/affine-desktop/ Dedicated config
E2E (mobile) Playwright tests/affine-mobile/ Dedicated config
E2E (copilot) Playwright tests/affine-cloud-copilot/ Dedicated config
BlockSuite E2E Playwright tests/blocksuite/ Dedicated config
Integration Vitest blocksuite/integration-test/ yarn test

Test utilities:

  • tests/kit/ — shared test helpers, Playwright fixtures, mock utilities
  • packages/backend/server/src/__tests__/utils/ — backend test app factory, mock user creation
  • Mockers class — factory for creating test users, workspaces, workspace members

Note

E2E tests use Playwright 1.58.2. The test infrastructure supports optional Istanbul coverage collection via COVERAGE=true and CPU throttling via CPU_THROTTLE environment variable.

Code Conventions

Convention Rule
File naming kebab-case (enforced by oxlint unicorn/filename-case)
Import order Sorted by simple-import-sort
Type imports import type { ... } enforced by typescript/consistent-type-imports
No barrel re-exports from dist/src no-restricted-imports rule prevents deep imports
Commit messages Commitlint enforced (tools/commitlint/)
Formatting Prettier (JS/TS), taplo (TOML), cargo fmt (Rust)
Module style NestJS decorators for backend, DI Service classes for frontend

Build System

# Development
yarn dev                    # Start dev server
yarn build                  # Production build
yarn typecheck              # TypeScript type checking
yarn lint                   # Run all linters (oxlint + eslint + prettier)
yarn lint:fix               # Auto-fix lint issues

# Testing
yarn test                   # Run unit tests
yarn test:ui                # Run tests with Vitest UI
yarn test:coverage          # Run tests with coverage

# Post-install
yarn affine init            # Initialize workspace (runs postinstall)

Build config is managed by tools/cli/ which provides the yarn affine command (aliased as yarn af). This handles development server startup, production bundling, and initialization.

Tip

Use DISABLE_DEV_OVERLAY=true to suppress the development overlay. Set SELF_HOSTED=true when building for self-hosted distribution.

Native Modules (Rust)

The project includes Rust native modules compiled via NAPI-RS:

Module Path Purpose
Frontend native packages/frontend/native/ SQLite, schema validation, media capture
Backend native packages/backend/native/ Server-side native operations
y-octo packages/common/y-octo/ CRDT implementation
Common native packages/common/native/ Hashcash, shared utilities
Mobile native packages/frontend/mobile-native/ Mobile platform bridges

Native modules are built using the Rust 2024 edition with release profile optimizations (LTO, single codegen unit, symbol stripping).

S3-Compatible Storage

The packages/common/s3-compat/ module provides an S3-compatible storage adapter supporting:

  • AWS S3
  • Cloudflare R2
  • MinIO
  • Any S3-compatible object storage

This is used for blob storage (images, attachments, files) in cloud and self-hosted deployments.


9. Integration Points

GraphQL API

The server exposes a GraphQL API (when running graphql or allinone flavor) for:

  • Workspace management (CRUD, members, permissions)
  • Document operations
  • User authentication and account management
  • Configuration management (admin only)
  • Copilot AI interactions
  • Payment and subscription management
  • Server health and version information

Public endpoints (no auth required):

  • serverConfig query — server version, features, name

WebSocket Sync

Real-time document synchronization uses WebSocket connections (Socket.IO). The sync protocol is based on Yjs/y-octo CRDT updates:

  1. Client connects via WebSocket
  2. Initial sync: full document state exchange
  3. Incremental sync: delta updates broadcast to all connected clients
  4. Conflict resolution: automatic CRDT merge (no manual conflict resolution needed)

MCP (Model Context Protocol)

AFFiNE supports MCP for AI tool integration, allowing external AI agents to create and manipulate workspace documents via WebSocket API.

OAuth/OIDC Integration

Configure external identity providers via the OAuthModule:

  • Google OAuth
  • GitHub OAuth
  • Custom OIDC providers

OIDC configuration requires the provider to return email in token claims.

Email (SMTP)

The MailModule sends transactional emails (invitations, notifications) via configured SMTP. The MAILER_NAME environment variable controls the HELO/EHLO name.

Warning

If MAILER_NAME is not set, nodemailer sends an empty HELO command, which strict SMTP servers reject. (Community issue #14842)

Payment Integration

The PaymentModule integrates with Stripe for subscription and per-feature billing. Requires stripe npm package and API keys configured in the server.

Calendar Integration

The CalendarModule (packages/backend/server/src/plugins/calendar/providers/) provides calendar event integration with external providers.