Architecture Overview
System Architecture
Inkweld is a monorepo application with distinct frontend and backend services:
Frontend
Technology Stack
- Framework: Angular with standalone components
- State Management: Service-based with RxJS
- Offline Storage: IndexedDB via y-indexeddb
- Real-Time: Yjs + y-websocket provider
- Editor: ProseMirror with y-prosemirror binding
- Build: Angular CLI + Vite
- Testing: Vitest (unit) + Playwright (e2e)
Modern Angular Patterns
// Dependency injection with inject()
export class MyComponent {
private projectService = inject(ProjectService);
private router = inject(Router);
}
// Modern control flow
@if (project) {
<app-editor [project]="project" />
} @else {
<app-loading />
}
@for (item of items; track item.id) {
<app-item [data]="item" />
}
Key Services
- ProjectStateService - Central project state management
- UnifiedProjectService - Hybrid online/offline operations
- DocumentService - Yjs document lifecycle
- WorldbuildingService - Template/schema system
- AuthService - Authentication and session management
Editor Schema
The ProseMirror schema is assembled in
frontend/src/app/components/element-ref/extended-schema.ts. It composes
three sources:
- ngx-editor's base nodes and marks (
@bobbyquantum/ngx-editor/schema) prosemirror-tables'tableNodes()output- Inkweld's own extensions from
@inkweld/prosemirror/schema— theelementRefnode plus thecomment,autoReview, and securelinkmarks
new Schema(...) is constructed in the frontend, never inside the shared
package. The shared package returns specs only; building the Schema there
would pull a second copy of prosemirror-model into the bundle and break the
class-identity checks in y-prosemirror and EditorView — typing would
silently stop working. prosemirror-tables is pinned in resolutions
alongside the other ProseMirror packages for the same reason.
Tables
Table support is layered on prosemirror-tables rather than on ngx-editor's
menu, because Inkweld replaced that menu with its own Material toolbar
(editor-toolbar.component.ts). Only the schema and the editing plugins come
from the library:
- Schema —
tableNodes()withcellContent: 'paragraph+'. Cells are restricted to paragraphs deliberately: allowing arbitrary blocks would permit nested tables and headings that no export format renders sensibly. An extraaligncell attribute carries GFM column alignment. - Plugins —
columnResizing,tableEditing, and aTab/Shift-Tabkeymap, appended to the editor's plugin list indocument.service.ts.columnResizingmust be registered beforetableEditing. - Wire format — table node names are block-level in
packages/inkweld-prosemirror/src/xml/tags.ts, so an empty cell serializes as<table_cell></table_cell>rather than collapsing to a self-closing tag and desynchronising the row. - Markdown —
markdownToXmlparses GFM tables andxmlToMarkdownemits them, so tables round-trip through the MCP tools and markdown export. - Publish — the HTML, EPUB, and Typst/PDF generators each render tables. Tables are not yet part of the user-configurable publish-styles system and currently get fixed built-in styling.
Backend (Bun + Hono)
Technology Stack
- Runtime: Bun (JavaScript runtime built for speed)
- Framework: Hono (lightweight web framework)
- Database: SQLite or D1 via Drizzle ORM
- Document Storage: LevelDB (per-project instances)
- Real-Time: Native WebSocket support
- Testing: Bun's built-in test runner
API Architecture
// Hono route example
app.get('/api/projects', authMiddleware, async (c) => {
const user = c.get('user');
const projects = await projectService.findByUser(user.id);
return c.json(projects);
});
// WebSocket upgrade
app.get('/ws/:projectId', async (c) => {
const projectId = c.req.param('projectId');
return wsHandler.upgrade(c.req.raw, projectId);
});
Key Modules
- auth/ - Session-based authentication
- routes/ - HTTP endpoints
- services/ - Business logic layer
- db/ - Drizzle ORM schema and database setup
- durable-objects/ - (Cloudflare Workers deployment only)
Real-Time Collaboration (Yjs)
CRDT Technology
Yjs uses Conflict-free Replicated Data Types (CRDTs) to enable:
- Concurrent editing without conflicts
- Eventual consistency across all clients
- Offline support with automatic merging
- Fine-grained updates (character-level)
Data Flow
Per-Project Storage
Each project gets its own LevelDB instance:
Benefits:
- Isolation - Projects don't interfere
- Scalability - Independent read/write operations
- Cleanup - Easy to delete project data
- Connection pooling - Automatic idle connection management
Database Schema
Drizzle ORM Schema
Document Storage Architecture
Authentication & Security
Session-Based Auth
- httpOnly cookies for CSRF protection
- Session store backed by Drizzle
- No JWT tokens (intentional design choice)
- Optional GitHub OAuth (configurable)
Security Measures
- CORS configuration via
ALLOWED_ORIGINS - CSRF protection on state-changing requests
- Content Security Policy headers
- Rate limiting (configurable)
- User approval workflow (optional)
Deployment Targets
Bun (Primary)
bun run dev # Development
bun run build # Production build
bun run start # Production server
Node.js (Compatible)
bun run build:node # Transpile for Node
node dist/node-runner.js
Cloudflare Workers (Experimental)
bun run build:worker
npx wrangler deploy
Requires:
- Durable Objects for WebSocket persistence
- D1 database binding
- R2 for file storage (optional)
Docker (Recommended for Production)
docker build -t inkweld .
docker run -p 8333:8333 -v inkweld_data:/data inkweld
Benefits:
- Single self-contained image with frontend embedded (~340MB)
- Automatic migrations on startup
- Volume mounting for persistence
- Health check endpoint
Build Pipeline
Development
npm run dev # Runs both frontend and backend
Powered by:
- Concurrently to run multiple processes
- Angular CLI dev server (port 4200)
- Bun runtime (port 8333)
Production
# Frontend
cd frontend
bun run build
bun run compress # Optional
# Backend
cd backend
bun run build
Output:
- frontend/dist/ - Angular production bundle
- backend/dist/ - Bun-optimized backend code
Docker Build
Multi-stage Dockerfile:
- Frontend build stage (Node.js)
- Backend build stage (Bun)
- Runtime stage (minimal Bun image)
- Copies frontend dist to static assets
- Includes migrations for auto-run
- Non-root user for security
Testing Strategy
Frontend Tests
npm test # Vitest unit tests
npm run e2e # Playwright e2e tests
- Unit tests with @ngneat/spectator
- E2E tests with fixtures (authenticatedPage, etc.)
- Mock API handlers in
e2e/mock-api/ - Screenshot tests for visual regression
Backend Tests
bun test
- Unit tests for services and utilities
- Integration tests for API endpoints
- Supertest for HTTP assertions
- In-memory SQLite for test isolation
API Documentation
OpenAPI Specification
Generated from code annotations:
cd backend
bun run generate:openapi
Output: backend/openapi.json
Client Generation
Auto-generate TypeScript client for frontend:
cd backend
bun run generate:angular-client
Output: frontend/src/api-client/
Never edit generated files manually.
Code Quality
Linting
- ESLint with TypeScript support
- Prettier for formatting
- Shared config across frontend/backend
Pre-commit Hooks
Consider adding:
- Lint-staged for fast checks
- Husky for Git hooks
- Prettier format check
CI/CD
GitHub Actions workflow:
- Lint all code
- Test frontend and backend
- Build Docker image
- Publish to GHCR (on main branch)
Performance Considerations
Frontend
- Lazy loading for routes
- OnPush change detection where appropriate
- Virtual scrolling for long lists (consider)
- Service Worker for offline support
Backend
- LevelDB connection pooling with automatic cleanup
- Database indexing on foreign keys
- Pagination for large result sets
- WebSocket connection limits (configurable)
Real-Time
- Debounced updates for UI refresh
- Incremental sync (only changed content)
- Efficient CRDT merging via Yjs
- Binary encoding over WebSocket
Development Workflow
Workspace Structure
NPM Scripts (Root)
bun install # Install all dependencies
npm run dev # Start dev servers
npm test # Run all tests
npm run lint # Lint all code
Git Workflow
- Create feature branch
- Make changes + add tests
- Run
npm testandnpm run lint - Open PR
- CI validates
- Merge to main
Extensibility
Custom Worldbuilding Templates
Worldbuilding element schemas (templates) are edited in-app through the unified
interactive schema editor: the WorldbuildingEditorComponent renders a live
preview of the schema (previewSchema + editMode inputs) and the owning
TemplateEditorPageComponent applies the resulting SchemaEditEvents to its
own schema state, autosaving via the schemaChange output.
The editor has two responsive layouts — a sidenav with sections on wide screens and a stacked accordion on narrow ones. In edit mode Schema Details is the top tab/section (name, icon, description), followed by the schema's tabs and the fixed Identity, Relationships, Media and Styling sections. Within the preview you can:
- add/remove/reorder tabs and fields,
- edit each field's config inline (label, type, placeholder, options, …),
- rename a tab and pick its icon from a curated Material-icon picker that covers every built-in element-type icon (so existing schemas' icons are always available),
- edit the schema name/icon/description under Schema Details,
- set the default appearance/image that new elements of this type get under Styling.
Changes save automatically; schema-design snapshots can be created and restored from the snapshot button.
interface ElementTypeSchema {
id: string; // nanoid, used for all lookups
name: string;
icon: string; // Material icon name
description: string;
version: number; // bumped on each save, used for migrations
tabs: TabSchema[];
defaultValues?: Record<string, unknown>;
defaultAppearance?: ElementAppearance; // menu/content backgrounds
defaultImage?: string; // media:// reference or URL
}
interface TabSchema {
key: string;
label: string;
icon?: string;
order?: number;
fields: FieldSchema[];
}
interface FieldSchema {
key: string;
label: string;
type:
| 'text'
| 'textarea'
| 'number'
| 'date'
| 'select'
| 'multiselect'
| 'array'
| 'checkbox';
placeholder?: string;
description?: string;
defaultValue?: unknown;
options?: string[] | { value: string; label: string }[];
validation?: FieldValidation;
layout?: { span?: number; order?: number };
}
Plugin System (Future)
Consider:
- Custom document types
- Export format plugins
- Theme customization
- Integration hooks (Discord, Slack)
Next Steps
- Review API documentation
- Read deployment guide
- Check the user guide
- Explore the features