architecture · git:20260103.9f62587 · 2026-01-03 · sha256 705622ad6abce4a5

architecture git:20260103.9f62587A

Immutable. This exact content is served forever at /api/v1/blob/705622ad6abce4a5.

---
description: Core architecture rules - layer separation, dependencies, and anti-patterns
globs: src/core/**/*
alwaysApply: false
---

# Architecture Rules

Rules for maintaining the layered architecture in `src/core/`. Based on ADR-0004, ADR-0008, ADR-0009.

## Layer Flow

```
UI (user actions) ──────┐
                        ↓
Coordinators (system) ─→ Controllers → Application → Services → Models
                         ↓              ↓             ↓
                         Stores         Pipes         Database
```

## Entry Points

Only these can initiate workflows:

| Entry Point | Trigger | Calls |
|-------------|---------|-------|
| **UI** | User actions (clicks, forms) | Controllers |
| **Coordinators** | System events (timers, auth, visibility) | Controllers |

## Layer Responsibilities

### Controllers (`src/core/controllers/`)
- ✅ Entry point for user-initiated actions
- ✅ Invoke pipes for normalization/validation
- ✅ Call application for business logic
- ✅ Mutate stores for UI state
- ❌ NEVER call services directly
- ❌ NEVER perform IO

### Coordinators (`src/core/coordinators/`)
- ✅ Entry point for system-initiated actions
- ✅ React to auth, visibility, route changes
- ✅ Call controllers (like UI does)
- ❌ NEVER call application directly
- ❌ NEVER call services directly

### Application (`src/core/*/application/`)
- ✅ Orchestrate business workflows
- ✅ Called BY controllers (NOT an entry point)
- ✅ Call services for IO
- ✅ Can call other Applications (with restrictions)
- ❌ NEVER access stores directly
- ❌ NEVER call controllers

### Services (`src/core/*/services/`)
- ✅ Handle IO boundaries
- ✅ `local/` - Dexie persistence
- ✅ `homeserver/` - Network writes (PUT/POST/DELETE)
- ✅ `nexus/` - Network reads
- ❌ NEVER call application or controllers
- ❌ NEVER access stores

### Pipes (`src/core/*/pipes/`)
- ✅ Normalize and validate data
- ✅ Transform external shapes to domain shapes
- ✅ Pure functions only
- ❌ NEVER perform IO
- ❌ NEVER access database or network

### Models (`src/core/*/models/`)
- ✅ Dexie-based persistence only
- ✅ CRUD operations on IndexedDB
- ❌ NEVER perform network calls
- ❌ NEVER access stores

## Application Cross-Domain Rules (ADR-0009)

Only these Applications can call other Applications:
- `PostApplication`
- `UserApplication`
- `NotificationApplication`

### Restrictions
```typescript
// ✅ ALLOWED: Orchestrators can call helper applications
PostApplication.createWithAttachments() {
  await FileApplication.upload(files);      // OK
  await TagApplication.commitCreate(tags);  // OK
}

// ❌ FORBIDDEN: Helper applications cannot call others
FileApplication.upload() {
  await TagApplication.create(); // ❌ VIOLATION
  await PostApplication.get();   // ❌ VIOLATION
}

// ❌ FORBIDDEN: No circular dependencies
PostApplication → FileApplication → PostApplication  // ❌

// ❌ FORBIDDEN: Max call depth is 1
PostApplication → FileApplication → ImageProcessor  // ❌ Too deep
```

## Anti-Patterns to Catch

### ❌ Controller calling Service directly
```typescript
// BAD
class PostController {
  static create() {
    await LocalPostService.upsert(post); // ❌ Bypass application
  }
}

// GOOD
class PostController {
  static create() {
    await PostApplication.create(post); // ✅ Through application
  }
}
```

### ❌ Application accessing Store
```typescript
// BAD
class PostApplication {
  static create() {
    usePostStore.getState().setLoading(true); // ❌
  }
}

// GOOD - Controller handles store
class PostController {
  static create() {
    usePostStore.getState().setLoading(true);
    await PostApplication.create(post);
  }
}
```

### ❌ IO in Pipes
```typescript
// BAD
class PostPipe {
  static normalize(post) {
    const user = await LocalUserService.get(post.author); // ❌ IO!
  }
}

// GOOD
class PostPipe {
  static normalize(post) {
    return { ...post, id: `${post.author}:${post.postId}` }; // ✅ Pure
  }
}
```

### ❌ Coordinator calling Application directly
```typescript
// BAD
class NotificationCoordinator {
  static poll() {
    await NotificationApplication.fetch(); // ❌ Bypass controller
  }
}

// GOOD
class NotificationCoordinator {
  static poll() {
    await UserController.notifications(); // ✅ Through controller
  }
}
```

## File Organization

```
src/core/
├── controllers/           # Entry points for UI
├── coordinators/          # Entry points for system
├── [domain]/
│   ├── application/       # Business logic orchestration
│   ├── services/
│   │   ├── local/         # Dexie operations
│   │   ├── homeserver/    # Network writes
│   │   └── nexus/         # Network reads
│   ├── pipes/             # Data transformation
│   └── models/            # Dexie tables
├── stores/                # UI state (Zustand)
└── index.ts               # Public API
```

## Quick Checklist

When adding/modifying code in `src/core/`:

- [ ] Does it respect layer boundaries?
- [ ] Is Application called BY controller, not calling controller?
- [ ] Are Coordinators going through Controllers?
- [ ] Is IO only in Services?
- [ ] Are Pipes pure (no IO)?
- [ ] Does cross-domain call follow ADR-0009 rules?

---

**Reference**: `.cursor/adr/0004-layering-and-dependency-rules.md`, `.cursor/adr/0008-coordinators-layer.md`, `.cursor/adr/0009-application-cross-domain-orchestration.md`