Imported from michyul/mdl (
AGENTS.md). Install upstream withnpx skills add michyul/mdl. Copyright stays with the author.
MDL — AI Agent Onboarding
Welcome. This document gives you everything needed to contribute effectively to MDL
as an AI coding agent. Read CLAUDE.md first — this file extends it.
Mental model of the system
User opens diagram
└─► Frontend connects to Hocuspocus WebSocket (ws://.../collab/{diagramId})
└─► Hocuspocus loads Yjs document from PostgreSQL yjs_documents table
└─► ReactFlow renders nodes/edges from Yjs state
└─► User edits node → Yjs op broadcast to all clients
└─► Change middleware logs field-level diff to change_log
└─► User "commits" → snapshot saved to model_versions
AI Modeller:
User sends prompt → backend → LM Studio (tool-calling) → Y.applyUpdate() → broadcast to all clients
Key invariants to preserve
- Classification is enforced at every layer: API filter → RLS → Hocuspocus auth hook → frontend rendering. Never remove a classification check.
- Yjs is the single source of truth for live diagram state. PostgreSQL stores snapshots (versions) and the Hocuspocus binary document. Do not store live diagram positions in a REST resource.
- Metamodel spec is the schema. Element attributes are validated against the metamodel definition, not hardcoded. When adding element types, add them to the metamodel JSON, not to TypeScript types.
- All external input is Zod-validated. This includes API bodies, environment variables, and metamodel JSON files loaded at runtime.
- No
anyin TypeScript. Useunknownwith type guards, or specific union types.
File-by-file guide
packages/shared-types/src/
The contract layer between frontend and backend.
metamodel.ts— the schema for metamodel JSON files. Change this when adding new metamodel concepts.models.ts— EA model, element, relationship, diagram, version types.api.ts— request/response shapes and generic API wrappers.classification.ts— classification level types and defaults.
Rule: When changing a Zod schema here, update both backend and frontend consumers.
apps/backend/src/db/schema/
Drizzle table definitions. One file per domain.
auth.ts— users, orgs, roles, tokensclassifications.ts— classification levelsmetamodels.ts— metamodel storagemodels.ts— EA models, elements, relationships, versions, change_logdiagrams.ts— diagrams, placements, groupscollaboration.ts— Yjs document binary storageai.ts— AI settings and sessions
Rule: After any schema change, run pnpm db:generate to create a migration.
apps/backend/src/api/<feature>/routes.ts
Each feature module registers its own Fastify routes. Pattern:
export const featureRoutes: FastifyPluginAsync = async (fastify) => {
fastify.get('/path', { preHandler: [fastify.authenticate] }, async (req, reply) => {
// 1. Validate input with Zod
// 2. Check clearance if returning classified data
// 3. Query with Drizzle
// 4. Return { ok: true, data: ... }
})
}
apps/backend/src/collab/hocuspocus.ts
The real-time collaboration engine. Three critical hooks:
onAuthenticate— validate JWT, check clearance for the diagramonLoadDocument— fetch Yjs binary fromyjs_documentstableonStoreDocument— persist updated Yjs binaryonChange— diff and log field-level changes tochange_log
Do not add business logic here. Call services.
apps/frontend/src/components/diagram/
ReactFlow canvas components. Each EA element type has a custom node component. Pattern for custom nodes:
export const ApplicationNode = memo(({ data, selected }: NodeProps<ApplicationNodeData>) => {
return (
<div className={cn('ea-node ea-node--application', selected && 'ea-node--selected')}>
{/* render using metamodel notation */}
</div>
)
})
ApplicationNode.displayName = 'ApplicationNode'
apps/frontend/src/stores/
Zustand stores. Keep them lean — only client-side state.
auth.ts— current user + token (persisted to localStorage)diagram.ts— current diagram UI state (selected elements, active layer, tool mode)ui.ts— sidebar open/close, toast notifications
How metamodels work
A metamodel is a JSON document conforming to MetamodelSpec (in packages/shared-types/src/metamodel.ts).
It defines:
- layers — architecture domains (Business, Application, Technology, Physical, etc.)
- elementTypes — node types with their attributes and notation (shape, colour, icon)
- relationshipTypes — edge types with valid source/target pairs and notation
- viewpoints — curated combinations of layers + element types for specific stakeholders
Built-in metamodels live in metamodels/ as JSON files and are seeded into the database on first run.
Users can fork a built-in metamodel to create an org-specific variant. The parentId column tracks the lineage.
When rendering a diagram, the frontend:
- Fetches the metamodel for the model
- Registers
nodeTypesin ReactFlow mapping MetaElementType.id → custom React component - Renders edges using the RelationshipType notation settings
Common tasks
Add a new EA element type to TOGAF
- Open
metamodels/togaf-adm.json - Add an entry to the
elementTypesarray following the existing pattern - Create a React component in
apps/frontend/src/components/diagram/nodes/ - Register it in the nodeTypes map in
apps/frontend/src/components/diagram/DiagramCanvas.tsx
Add a new API endpoint
- Define request/response types in
packages/shared-types/src/api.ts - Implement service logic in
apps/backend/src/services/<feature>.ts - Add route in
apps/backend/src/api/<feature>/routes.ts - Register with
fastify.register(featureRoutes, { prefix: '/feature' })insrc/index.ts - Add TanStack Query hook in
apps/frontend/src/services/<feature>.ts
Implement a new AI tool
AI tools are defined in apps/backend/src/services/ai-modeller.ts.
Each tool is an OpenAI-compatible function definition + handler.
The handler applies changes via Yjs → they broadcast to all connected clients automatically.
Testing guidance
- Unit tests go next to the file they test:
myService.test.ts - Integration tests for API routes go in
apps/backend/src/api/<feature>/routes.test.ts - Run all tests:
pnpm test - Run with coverage:
pnpm test -- --coverage - Test files use Vitest. Do not use Jest.
Things to never do
- Never skip classification checks, even in tests
- Never call
Y.Docdirectly in API routes — always go through Hocuspocus - Never store secrets in code or JSON files
- Never add
anyto TypeScript — this is a hard lint error - Never modify generated migration files in
src/db/migrations/ - Never change the built-in metamodel JSON IDs (they are stable FK targets)
- Never bypass the Zod validation layer for API inputs