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:
- User types in BlockSuite editor → block model updates via Yjs document
- y-octo (Rust CRDT) processes the update locally
- Update is persisted to local SQLite (desktop) or IndexedDB (web)
- If cloud sync is enabled, update is sent via WebSocket to the sync server
- Server merges CRDT updates and broadcasts to other connected clients
Server API requests:
- Client sends GraphQL query/mutation via HTTPS
- NestJS resolves the request through configured modules
- Prisma executes database operations against PostgreSQL
- 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
packageManagerfield inpackage.jsonenforcesyarn@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):
- Database-stored config (set via GraphQL
updateAppConfigmutation) - JSON config file (
config.jsonin project root or~/.affine/config/config.json) - Environment variables
- 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
allinonemode, 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
Adminrole can accessappConfigqueries 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
DataMigrationtable. Each migration hasupanddownmethods 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_KEYin 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
UpgradeandConnectionheaders for the/socket.iopath.
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
/infoendpoint 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 returnnull.
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-clsfor 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/transactionaladapter 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 lengthmax: 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-Idheader) 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_KEYmust 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:imageaffine:bookmark,affine:attachment,affine:calloutaffine:divider,affine:latex,affine:embedaffine:database,affine:data-view,affine:tableaffine:surface,affine:surface-ref,affine:noteaffine: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 usersGraphQLError— 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 utilitiespackages/backend/server/src/__tests__/utils/— backend test app factory, mock user creationMockersclass — 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=trueand CPU throttling viaCPU_THROTTLEenvironment 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=trueto suppress the development overlay. SetSELF_HOSTED=truewhen 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):
serverConfigquery — 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:
- Client connects via WebSocket
- Initial sync: full document state exchange
- Incremental sync: delta updates broadcast to all connected clients
- 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_NAMEis 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.