* add contributing md and coding agents md docs * remove contributing md from this branch * minor updates * resolve PR comments and add tests instructions
15 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Colanode is an open-source, local-first collaboration platform supporting real-time chat, rich text editing, customizable databases, and file management. It uses a sophisticated CRDT-based architecture (powered by Yjs) to enable offline-first operation with automatic conflict resolution.
Commands
Development
# Install dependencies (also runs postinstall script to generate emoji/icon assets)
npm install
Prefer running dev/build/compile/format commands inside the specific app or package directory.
Note: Tests exist for apps/server and apps/web and are run with Vitest. Prefer running tests in the relevant app directory; npm run test at the repo root runs the same suites via Turbo.
Individual App Development
Server:
cd apps/server
cp .env.example .env # Configure environment variables
npm run dev # Start server with hot reload
# Start dependencies (Postgres, Redis, Mail server) via Docker Compose
docker compose -f hosting/docker/docker-compose.yaml up -d
# Include MinIO (S3-compatible storage) for testing
docker compose -f hosting/docker/docker-compose.yaml --profile s3 up -d
Web:
cd apps/web
npm run dev
Desktop:
cd apps/desktop
npm run dev
Utility Scripts
Located in scripts/:
cd scripts
# Generate emoji assets (run from scripts directory)
npm run generate:emojis
# Generate icon assets (run from scripts directory)
npm run generate:icons
# Seed database with test data (run from scripts directory)
npm run seed
Note: Generated emoji and icon assets are git-ignored. These are regenerated on npm install via the postinstall hook.
Architecture
Monorepo Structure
This is a Turborepo monorepo with npm workspaces:
packages/core- Shared types, validation schemas (Zod), business rules, and node type registry. Foundation for all other packages.packages/crdt- CRDT implementation wrapping Yjs. Provides type-safe document updates and conflict-free merging.packages/client- Client-side services, local SQLite database schema, and API communication layer. Handles mutations, synchronizers, and offline support.packages/ui- React components using TailwindCSS, Radix UI primitives, and TipTap editor. Shared between web and desktop apps.apps/server- Fastify-based API server with WebSocket support. Manages Postgres database, Redis events, and background jobs.apps/web- Web application (Vite + React + TanStack Router).apps/desktop- Electron desktop application.apps/mobile- React Native mobile app (experimental, not production-ready).scripts/- Utility scripts for generating emojis, icons, and seed data.
Local-First Architecture
Core Principle: All data operations happen locally first, then sync to the server in the background.
Client Write Path:
- User makes a change (e.g., edits a document)
- Change is immediately applied to local SQLite database
- CRDT update is generated using Yjs (as binary
Uint8Array) - Update is stored in
mutationstable as a pending operation MutationServicebatches and sends mutations to server via HTTP POST- Server validates, applies updates, and stores in Postgres
- Server broadcasts changes to other clients via WebSocket
Client Read Path:
- All reads happen from local SQLite (instant response)
Synchronizerservices pull updates from server via WebSocket- Updates are applied to local database in background
- UI reactively updates when local data changes
Key Files:
packages/client/src/services/workspaces/mutation-service.ts- Mutation batching/syncingpackages/client/src/services/workspaces/synchronizer.ts- Real-time sync via WebSocketpackages/client/src/databases/workspace/schema.ts- Local SQLite schemaapps/server/src/api/client/routes/workspaces/mutations/mutations-sync.ts- Server mutation endpointapps/server/src/services/socket-connection.ts- WebSocket connection handler
CRDT Integration (Yjs)
Purpose: Enable conflict-free collaborative editing with automatic merge resolution.
Implementation:
packages/crdt/src/index.tscontains theYDocclass wrapping Yjs documents- Each node (page, database record, etc.) has a corresponding Yjs document
- Updates are encoded as binary blobs (Base64 for storage, Uint8Array for processing)
- Server merges concurrent updates automatically using Yjs CRDT semantics
Storage Strategy: Client storage maintains multiple layers:
- Current State - JSON representation of latest merged state (for querying/UI)
- Merged CRDT State - Binary Yjs state in
node_states/document_states - Pending Updates - Local, unsynced CRDT updates in
node_updates/document_updateskept separately so they can be reverted; once synced they are merged into*_statesand removed
Server storage keeps current JSON state (nodes / documents) plus CRDT update history (node_updates / document_updates); background jobs merge older updates.
Tables:
nodes/documents- Current state as JSON/JSONBnode_states/document_states- Merged CRDT state (client-side)node_updates/document_updates- Pending local CRDT updates (client-side) and CRDT update history (server-side)
Background Merging:
Server jobs periodically merge old updates to reduce storage (apps/server/src/jobs/node-updates-merge.ts).
Database Synchronization
Local (SQLite) ↔ Server (Postgres):
Cursor-based streaming synchronization:
- Each data stream (users, nodes, documents, collaborations, etc.) has a
Synchronizer - Synchronizers track a cursor (last synced revision number)
- Client requests updates via WebSocket:
synchronizer.input { cursor: 12345 } - Server responds with batch:
synchronizer.output { updates: [...], cursor: 12350 } - Client applies updates to local database and persists new cursor
Synchronizer Types:
users- User list changescollaborations- Access control updatesnode.updates- Node CRDT updates (per workspace root)document.updates- Document CRDT updates (per workspace root)node.reactions- Emoji reactionsnode.interactions- Read receipts and activity tracking
Key Files:
packages/client/src/services/workspaces/synchronizer.tsapps/server/src/synchronizers/*- Server-side data fetchersapps/server/src/lib/event-bus.ts- Event system for triggering syncs
Node Type Registry
Location: packages/core/src/registry/nodes/
Each node type (space, page, database, message, etc.) defines:
- Attribute Schema - Zod schema for node metadata
- Document Schema (optional) - Zod schema for collaborative content
- Permission Checks -
canCreate,canUpdate,canDelete,canRead - Text Extraction - For search indexing
- Mention Extraction - For @mentions and notifications
Example Node Types:
space- Top-level containerpage- Rich text documentdatabase- Structured data with custom fields and viewsrecord- Database rowdatabase_view- Saved database viewchat- Chat containerchannel- Chat channelmessage- Chat messagefile- File attachmentfolder- Organizational container
Important: When adding a new node type, register it in the appropriate registry file and ensure both client and server import it.
Configuration System
Server Configuration:
The server uses a JSON-based configuration system with smart reference resolution:
- Config File Location: Set via
CONFIGenvironment variable (e.g.,CONFIG=/path/to/config.json) - Default Behavior: If
CONFIGis not set, server uses schema defaults fromapps/server/src/lib/config/ - Example Config: See
apps/server/config.example.jsonfor a complete template
Reference Resolution:
The config system supports special prefixes for dynamic value loading:
env://VAR_NAME- Resolves to environment variable at runtime (required, fails if not set)file://path/to/file- Reads and inlines file contents at runtime (useful for certificates/secrets)- Direct values - Plain strings/numbers/booleans in JSON
Example:
{
"postgres": {
"url": "env://POSTGRES_URL",
"ssl": {
"ca": "file:///secrets/postgres-ca.pem"
}
},
"storage": {
"provider": {
"type": "s3",
"endpoint": "env://S3_ENDPOINT",
"accessKey": "env://S3_ACCESS_KEY",
"secretKey": "env://S3_SECRET_KEY"
}
}
}
Required Configuration:
- Only
postgres.urlis truly required (defaults toenv://POSTGRES_URL) - All other settings have sensible defaults in the Zod schemas
- Storage defaults to
filetype with./datadirectory - Redis defaults to
env://REDIS_URLbut has fallback behavior
For Docker/Production:
- Copy
apps/server/config.example.jsonto your own config file - Update values (use
env://for secrets, direct values for non-sensitive settings) - Mount your config file and set
CONFIG=/path/to/mounted/config.json - Set required environment variables referenced by
env://pointers
For Local Development:
- Use
.envfile inapps/server/directory - Server will use schema defaults if no config file is provided
- See
apps/server/.env.examplefor available environment variables
Config Validation:
- All config is validated using Zod schemas at startup
- Validation errors show clear messages about missing/invalid values
- Server exits immediately if config is invalid
Client Apps:
- Use standard
.envfiles for build-time configuration - Runtime configuration fetched from server
Testing
Automated Tests (Vitest):
apps/server:npm run testapps/web:npm run test- Repo root:
npm run test(runs app tests via Turbo) - Focus manual verification on areas without test coverage
- Include clear verification steps in pull request descriptions
Development Tips
Working with CRDTs
When modifying node or document schemas:
- Update Zod schema in
packages/core/src/registry/nodes/<type>.ts - The CRDT layer automatically handles schema validation via
YDoc.update() - Test with multiple clients to verify conflict resolution
- Remember: updates are append-only, deletions use tombstones
Debugging Synchronization
Client-side:
- Check
mutationstable for pending operations - Check
cursorstable for sync position - Use browser DevTools WebSocket tab to inspect messages
Server-side:
- Logs are in JSON format (Pino logger)
- Look for
synchronizer.input/synchronizer.outputmessages - Check
node_updatestable for stored updates - Verify revision numbers are incrementing
Database Migrations
Server (Postgres):
- Schema defined in
apps/server/src/data/schema.ts - Migrations live in
apps/server/src/data/migrationsand run via the Kysely migrator inapps/server/src/data/database.ts - Add a migration for schema changes; avoid manual production edits. For local dev, dropping the DB is a last resort.
Client (SQLite):
- Schema versioning handled in
packages/client/src/databases/workspace/schema.ts - Migration logic in
packages/client/src/databases/workspace/migrations.ts - Migrations run automatically on app startup
Adding a New Node Type
- Create schema in
packages/core/src/registry/nodes/<type>.ts - Define attribute schema, document schema (if collaborative), and permissions
- Register in
packages/core/src/registry/nodes/index.ts - Update server-side node creation logic in
apps/server/src/lib/nodes.ts - Add client-side service in
packages/client/src/services/ - Create UI components in
packages/ui/src/components/
Storage Backends
Server supports multiple storage backends for files:
- File (default) - Local filesystem storage in
./datadirectory - S3 - AWS S3 or compatible (MinIO, DigitalOcean Spaces, etc.)
- GCS - Google Cloud Storage
- Azure - Azure Blob Storage
Configuration:
Configure via storage.provider.type in your config file. Examples:
Filesystem (default):
{
"storage": {
"provider": {
"type": "file",
"directory": "./data"
}
}
}
S3-compatible:
{
"storage": {
"provider": {
"type": "s3",
"endpoint": "env://S3_ENDPOINT",
"accessKey": "env://S3_ACCESS_KEY",
"secretKey": "env://S3_SECRET_KEY",
"bucket": "env://S3_BUCKET",
"region": "env://S3_REGION",
"forcePathStyle": false
}
}
}
GCS:
{
"storage": {
"provider": {
"type": "gcs",
"bucket": "env://GCS_BUCKET",
"projectId": "env://GCS_PROJECT_ID",
"credentials": "file:///secrets/gcs-credentials.json"
}
}
}
Azure:
{
"storage": {
"provider": {
"type": "azure",
"account": "env://AZURE_STORAGE_ACCOUNT",
"accountKey": "env://AZURE_STORAGE_ACCOUNT_KEY",
"containerName": "env://AZURE_CONTAINER_NAME"
}
}
}
See apps/server/src/lib/storage/ for implementations and apps/server/src/lib/config/storage.ts for full schema.
Common Patterns
Service Layer Pattern
Services encapsulate business logic and coordinate between database and API:
packages/client/src/services/- Client-side servicesapps/server/src/services/- Server-side services
Repository Pattern
Database access abstracted through clear interfaces:
packages/client/src/databases/- Client database layerapps/server/src/data/- Server database layer
Event-Driven Updates
- Client: Uses TanStack DB for reactive data fetching alongside TanStack Query
- Server: Uses EventBus (Redis-backed) for cross-instance communication
- Background jobs via BullMQ for asynchronous processing
Optimistic Updates
All mutations are optimistic:
- Update local state immediately
- Show UI change instantly
- Send mutation to server in background
- On failure (after 10 retries), show error and optionally revert
- CRDT ensures eventual consistency even if server order differs
Important Considerations
- Generated Assets: Emoji and icon files are generated during
npm install. Don't commit these to git. - TypeScript Source Imports: Packages use TypeScript source directly (not compiled outputs) during development for faster iteration.
- Local-First Mindset: Always assume network can fail. Design features to work offline first.
- CRDT Limitations: Not all data types use CRDTs (e.g., messages and files use simpler database tables).
- Mobile App: The
apps/mobileis experimental and not production-ready. - Performance: For large workspaces, synchronizers per root node can cause memory pressure. Monitor closely.