# Domain-Driven Design Analysis: Smesh Nostr Client

## Executive Summary

This document provides a Domain-Driven Design (DDD) analysis of the Smesh codebase, a React/TypeScript Nostr client. The analysis identifies the implicit domain model, evaluates current architecture against DDD principles, and provides actionable recommendations for refactoring toward a more domain-centric design.

**Key Findings:**
- The codebase has implicit bounded contexts but lacks explicit boundaries
- Domain logic is scattered across providers, services, and lib utilities
- The architecture exhibits several DDD anti-patterns (Anemic Domain Model, Smart UI tendencies)
- Nostr events naturally align with Domain Events pattern
- Strong foundation exists for incremental DDD adoption

---

## 1. Domain Analysis

### 1.1 Core Domain Identification

The Smesh application operates in the **decentralized social networking** domain, specifically implementing the Nostr protocol. The core business capabilities are:

| Subdomain | Type | Description |
|-----------|------|-------------|
| **Identity & Authentication** | Core | Key management, signing, account switching |
| **Social Graph** | Core | Following, muting, trust relationships |
| **Content Publishing** | Core | Notes, reactions, reposts, media |
| **Feed Curation** | Core | Timeline construction, filtering, relay selection |
| **Relay Management** | Supporting | Relay sets, discovery, connectivity |
| **Notifications** | Supporting | Real-time event monitoring |
| **Translation** | Generic | Multi-language content translation |
| **Media Upload** | Generic | NIP-96/Blossom file hosting |

### 1.2 Ubiquitous Language

The codebase uses Nostr protocol terminology, which forms the basis of the ubiquitous language:

| Term | Definition | Current Implementation |
|------|------------|----------------------|
| **Event** | Signed JSON object (note, reaction, etc.) | `nostr-tools` Event type |
| **Pubkey** | User's public key identifier | String (should be Value Object) |
| **Relay** | WebSocket server for event distribution | String URL (should be Value Object) |
| **Kind** | Event type identifier (0=profile, 1=note, etc.) | Number constants in `constants.ts` |
| **Tag** | Metadata attached to events (p, e, a tags) | String arrays (should be Value Objects) |
| **Profile** | User metadata (name, avatar, etc.) | `TProfile` type |
| **Follow List** | User's contact list (kind 3) | Array in `FollowListProvider` |
| **Mute List** | Blocked users/content (kind 10000) | Array in `MuteListProvider` |
| **Relay List** | User's preferred relays (kind 10002) | `TRelayList` type |
| **Signer** | Key management abstraction | `ISigner` interface |

**Language Issues Identified:**
- "Stuff Stats" is unclear domain terminology (rename to `InteractionMetrics`)
- "Favorite Relays" vs "Relay Sets" inconsistency
- "Draft Event" conflates unsigned events with work-in-progress content

---

## 2. Current Architecture Assessment

### 2.1 Directory Structure Analysis

```
src/
├── providers/     # State management + some domain logic (17 contexts)
├── services/      # Business logic + infrastructure concerns mixed
├── lib/           # Utility functions + domain logic mixed
├── types/         # Type definitions (implicit domain model)
├── components/    # UI components (some contain business logic)
├── pages/         # Page components
└── hooks/         # Custom React hooks
```

**Assessment:** The architecture follows a layered approach but lacks explicit domain layer separation. Domain logic is distributed across:
- `lib/` - Event manipulation, validation
- `services/` - Data fetching, caching, persistence
- `providers/` - State management with embedded business rules

### 2.2 Implicit Bounded Contexts

The codebase contains several implicit bounded contexts that could be made explicit:

```
┌─────────────────────────────────────────────────────────────────┐
│                        CONTEXT MAP                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐      Partnership      ┌──────────────┐        │
│  │   Identity   │◄────────────────────►│ Social Graph │        │
│  │   Context    │                       │   Context    │        │
│  └──────┬───────┘                       └──────┬───────┘        │
│         │                                      │                 │
│         │ Customer/Supplier                    │                 │
│         ▼                                      ▼                 │
│  ┌──────────────┐                       ┌──────────────┐        │
│  │   Content    │                       │    Feed      │        │
│  │   Context    │                       │   Context    │        │
│  └──────────────┘                       └──────────────┘        │
│         │                                      │                 │
│         └──────────────┬───────────────────────┘                │
│                        ▼                                         │
│                 ┌──────────────┐                                │
│                 │    Relay     │                                │
│                 │   Context    │                                │
│                 └──────────────┘                                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

**Context Descriptions:**

1. **Identity Context**
   - Concerns: Key management, signing, account switching
   - Current: `NostrProvider`, `ISigner` implementations
   - Entities: Account, Signer

2. **Social Graph Context**
   - Concerns: Following, muting, trust, pinned users
   - Current: `FollowListProvider`, `MuteListProvider`, `UserTrustProvider`
   - Entities: User, FollowList, MuteList

3. **Content Context**
   - Concerns: Creating and publishing events
   - Current: `lib/draft-event.ts`, publishing logic in providers
   - Entities: Note, Reaction, Repost, Bookmark

4. **Feed Context**
   - Concerns: Timeline construction, filtering, display
   - Current: `FeedProvider`, `KindFilterProvider`, `NotificationProvider`
   - Entities: Feed, Filter, Timeline

5. **Relay Context**
   - Concerns: Relay management, connectivity, selection
   - Current: `FavoriteRelaysProvider`, `ClientService`
   - Entities: Relay, RelaySet, RelayList

---

## 3. Anti-Pattern Analysis

### 3.1 Anemic Domain Model

**Severity: High**

The current implementation exhibits a classic anemic domain model. Domain types are primarily data structures without behavior.

**Evidence:**

```typescript
// Current: Types are data containers (src/types/index.d.ts)
type TProfile = {
  pubkey: string
  username?: string
  displayName?: string
  avatar?: string
  // ... no behavior
}

// Business logic lives in external functions (src/lib/event-metadata.ts)
export function extractProfileFromEventContent(event: Event): TProfile {
  // Logic external to the domain object
}
```

**Impact:**
- Business rules scattered across `lib/`, `services/`, `providers/`
- Difficult to find all rules related to a concept
- Easy to bypass validation by directly manipulating data

### 3.2 Smart UI Tendencies

**Severity: Medium**

Some business logic exists in UI components and providers that should be in domain layer.

**Evidence:**

```typescript
// Provider contains domain logic (src/providers/FollowListProvider.tsx)
const follow = async (pubkey: string) => {
  // Business rule: can't follow yourself
  if (pubkey === currentPubkey) return

  // Business rule: avoid duplicates
  if (followList.includes(pubkey)) return

  // Event creation and publishing
  const newFollowList = [...followList, pubkey]
  const draftEvent = createFollowListDraftEvent(...)
  await publish(draftEvent)
}
```

This logic belongs in a domain service or aggregate, not in a React context provider.

### 3.3 Database-Driven Design Elements

**Severity: Low**

The `IndexedDB` schema influences some type definitions, though this is less severe than traditional database-driven design.

**Evidence:**
- Storage keys defined alongside domain constants
- Some types mirror storage structure rather than domain concepts

### 3.4 Missing Aggregate Boundaries

**Severity: Medium**

No explicit aggregate roots or boundaries exist. Related data is managed independently.

**Evidence:**
- `FollowList`, `MuteList`, `PinList` are managed by separate providers
- No transactional consistency guarantees
- Cross-cutting updates happen independently

### 3.5 Leaky Abstractions

**Severity: Medium**

Infrastructure concerns leak into what should be domain logic.

**Evidence:**

```typescript
// Service mixes domain and infrastructure (src/services/client.service.ts)
class ClientService extends EventTarget {
  private pool = new SimplePool()           // Infrastructure
  private cache = new LRUCache(...)         // Infrastructure
  private userIndex = new FlexSearch(...)   // Infrastructure

  // Domain logic mixed with caching, batching, retries
  async fetchProfile(pubkey: string): Promise<TProfile | null> {
    // Caching logic
    // Relay selection logic (domain)
    // Network calls (infrastructure)
    // Index updates (infrastructure)
  }
}
```

---

## 4. Current Strengths

### 4.1 Natural Domain Event Alignment

Nostr events ARE domain events. The protocol's event-sourced nature aligns perfectly with DDD:

```typescript
// Nostr events capture domain facts
{
  kind: 1,                    // Note created
  content: "Hello Nostr!",
  tags: [["p", "..."]],       // Mentions
  created_at: 1234567890,
  pubkey: "...",
  sig: "..."
}
```

### 4.2 Signer Interface Abstraction

The `ISigner` interface is a well-designed port in hexagonal architecture terms:

```typescript
interface ISigner {
  getPublicKey(): Promise<string>
  signEvent(draftEvent: TDraftEvent): Promise<VerifiedEvent>
  nip04Encrypt(pubkey: string, plainText: string): Promise<string>
  nip04Decrypt(pubkey: string, cipherText: string): Promise<string>
}
```

Multiple implementations exist: `NsecSigner`, `Nip07Signer`, `BunkerSigner`, etc.

### 4.3 Event Creation Factories

The `lib/draft-event.ts` file contains factory functions that encapsulate event creation:

```typescript
createShortTextNoteDraftEvent(content, tags?, relays?)
createReactionDraftEvent(event, emoji?)
createFollowListDraftEvent(tags, content?)
createBookmarkDraftEvent(tags, content?)
```

These are proto-factories that could be formalized into proper Factory patterns.

### 4.4 Clear Type Definitions

The `types/index.d.ts` file provides a foundation for a rich domain model, even if currently anemic.

---

## 5. Refactoring Recommendations

### 5.1 Phase 1: Establish Domain Layer (Low Risk)

**Goal:** Create explicit domain layer without disrupting existing functionality.

**Actions:**

1. **Create domain directory structure:**

```
src/
├── domain/
│   ├── identity/
│   │   ├── Account.ts
│   │   ├── Pubkey.ts (Value Object)
│   │   └── index.ts
│   ├── social/
│   │   ├── FollowList.ts (Aggregate)
│   │   ├── MuteList.ts (Aggregate)
│   │   └── index.ts
│   ├── content/
│   │   ├── Note.ts (Entity)
│   │   ├── Reaction.ts (Value Object)
│   │   └── index.ts
│   ├── relay/
│   │   ├── Relay.ts (Value Object)
│   │   ├── RelaySet.ts (Aggregate)
│   │   └── index.ts
│   └── shared/
│       ├── EventId.ts
│       ├── Timestamp.ts
│       └── index.ts
```

2. **Introduce Value Objects for primitives:**

```typescript
// src/domain/identity/Pubkey.ts
export class Pubkey {
  private constructor(private readonly value: string) {}

  static fromHex(hex: string): Pubkey {
    if (!/^[0-9a-f]{64}$/i.test(hex)) {
      throw new InvalidPubkeyError(hex)
    }
    return new Pubkey(hex)
  }

  static fromNpub(npub: string): Pubkey {
    const decoded = nip19.decode(npub)
    if (decoded.type !== 'npub') {
      throw new InvalidPubkeyError(npub)
    }
    return new Pubkey(decoded.data)
  }

  toHex(): string { return this.value }
  toNpub(): string { return nip19.npubEncode(this.value) }

  equals(other: Pubkey): boolean {
    return this.value === other.value
  }
}
```

```typescript
// src/domain/relay/RelayUrl.ts
export class RelayUrl {
  private constructor(private readonly value: string) {}

  static create(url: string): RelayUrl {
    const normalized = normalizeRelayUrl(url)
    if (!isValidRelayUrl(normalized)) {
      throw new InvalidRelayUrlError(url)
    }
    return new RelayUrl(normalized)
  }

  toString(): string { return this.value }

  equals(other: RelayUrl): boolean {
    return this.value === other.value
  }
}
```

3. **Create rich domain entities:**

```typescript
// src/domain/social/FollowList.ts
export class FollowList {
  private constructor(
    private readonly _ownerPubkey: Pubkey,
    private _following: Set<string>,
    private _petnames: Map<string, string>
  ) {}

  static empty(owner: Pubkey): FollowList {
    return new FollowList(owner, new Set(), new Map())
  }

  static fromEvent(event: Event): FollowList {
    // Reconstitute from Nostr event
  }

  follow(pubkey: Pubkey): FollowListUpdated {
    if (pubkey.equals(this._ownerPubkey)) {
      throw new CannotFollowSelfError()
    }
    if (this._following.has(pubkey.toHex())) {
      return FollowListUpdated.noChange()
    }
    this._following.add(pubkey.toHex())
    return FollowListUpdated.added(pubkey)
  }

  unfollow(pubkey: Pubkey): FollowListUpdated {
    if (!this._following.has(pubkey.toHex())) {
      return FollowListUpdated.noChange()
    }
    this._following.delete(pubkey.toHex())
    return FollowListUpdated.removed(pubkey)
  }

  isFollowing(pubkey: Pubkey): boolean {
    return this._following.has(pubkey.toHex())
  }

  toDraftEvent(): TDraftEvent {
    // Convert to publishable event
  }
}
```

### 5.2 Phase 2: Introduce Domain Services (Medium Risk)

**Goal:** Extract business logic from providers into domain services.

**Actions:**

1. **Create domain services for cross-aggregate operations:**

```typescript
// src/domain/content/PublishingService.ts
export class PublishingService {
  constructor(
    private readonly relaySelector: RelaySelector,
    private readonly signer: ISigner
  ) {}

  async publishNote(
    content: string,
    mentions: Pubkey[],
    replyTo?: EventId
  ): Promise<PublishedNote> {
    const note = Note.create(content, mentions, replyTo)
    const relays = await this.relaySelector.selectForPublishing(note)
    const signedEvent = await this.signer.signEvent(note.toDraftEvent())

    return new PublishedNote(signedEvent, relays)
  }
}
```

```typescript
// src/domain/relay/RelaySelector.ts
export class RelaySelector {
  constructor(
    private readonly userRelayList: RelayList,
    private readonly mentionRelayResolver: MentionRelayResolver
  ) {}

  async selectForPublishing(note: Note): Promise<RelayUrl[]> {
    const writeRelays = this.userRelayList.writeRelays()
    const mentionRelays = await this.resolveMentionRelays(note.mentions)

    return this.mergeAndDeduplicate(writeRelays, mentionRelays)
  }
}
```

2. **Refactor providers to use domain services:**

```typescript
// src/providers/ContentProvider.tsx (refactored)
export function ContentProvider({ children }: Props) {
  const { signer, relayList } = useNostr()

  // Domain service instantiation
  const publishingService = useMemo(
    () => new PublishingService(
      new RelaySelector(relayList, new MentionRelayResolver()),
      signer
    ),
    [signer, relayList]
  )

  const publishNote = useCallback(async (content: string, mentions: string[]) => {
    const pubkeys = mentions.map(Pubkey.fromHex)
    const result = await publishingService.publishNote(content, pubkeys)
    // Update UI state
  }, [publishingService])

  return (
    <ContentContext.Provider value={{ publishNote }}>
      {children}
    </ContentContext.Provider>
  )
}
```

### 5.3 Phase 3: Define Aggregate Boundaries (Higher Risk)

**Goal:** Establish clear aggregate roots with transactional boundaries.

**Proposed Aggregates:**

| Aggregate Root | Child Entities | Invariants |
|----------------|----------------|------------|
| `UserProfile` | Profile metadata | NIP-05 validation |
| `FollowList` | Follow entries, petnames | No self-follow, unique entries |
| `MuteList` | Public mutes, private mutes | Encryption for private |
| `RelaySet` | Relay URLs, names | Valid URLs, unique within set |
| `Bookmark` | Bookmarked events | Unique event references |

**Implementation:**

```typescript
// src/domain/social/FollowList.ts (Aggregate Root)
export class FollowList {
  private _domainEvents: DomainEvent[] = []

  follow(pubkey: Pubkey): void {
    // Invariant enforcement
    this.ensureNotSelf(pubkey)
    this.ensureNotAlreadyFollowing(pubkey)

    this._following.add(pubkey.toHex())

    // Raise domain event
    this._domainEvents.push(
      new UserFollowed(this._ownerPubkey, pubkey, new Date())
    )
  }

  pullDomainEvents(): DomainEvent[] {
    const events = [...this._domainEvents]
    this._domainEvents = []
    return events
  }
}
```

### 5.4 Phase 4: Introduce Repositories (Higher Risk)

**Goal:** Abstract persistence behind domain-focused interfaces.

```typescript
// src/domain/social/FollowListRepository.ts (Interface in domain)
export interface FollowListRepository {
  findByOwner(pubkey: Pubkey): Promise<FollowList | null>
  save(followList: FollowList): Promise<void>
}

// src/infrastructure/persistence/IndexedDbFollowListRepository.ts
export class IndexedDbFollowListRepository implements FollowListRepository {
  constructor(
    private readonly indexedDb: IndexedDbService,
    private readonly clientService: ClientService
  ) {}

  async findByOwner(pubkey: Pubkey): Promise<FollowList | null> {
    // Check IndexedDB cache
    const cached = await this.indexedDb.getFollowList(pubkey.toHex())
    if (cached) {
      return FollowList.fromEvent(cached)
    }

    // Fetch from relays
    const event = await this.clientService.fetchFollowList(pubkey.toHex())
    if (event) {
      await this.indexedDb.saveFollowList(event)
      return FollowList.fromEvent(event)
    }

    return null
  }

  async save(followList: FollowList): Promise<void> {
    const draftEvent = followList.toDraftEvent()
    // Sign and publish handled by application service
  }
}
```

### 5.5 Phase 5: Event-Driven Architecture (Advanced)

**Goal:** Leverage Nostr's event-sourced nature for cross-context communication.

```typescript
// src/domain/shared/DomainEvent.ts
export abstract class DomainEvent {
  readonly occurredAt: Date = new Date()
  abstract get eventType(): string
}

// src/domain/social/events/UserFollowed.ts
export class UserFollowed extends DomainEvent {
  constructor(
    readonly follower: Pubkey,
    readonly followed: Pubkey,
    readonly timestamp: Date
  ) {
    super()
  }

  get eventType(): string { return 'social.user_followed' }
}

// src/application/handlers/UserFollowedHandler.ts
export class UserFollowedHandler {
  constructor(
    private readonly notificationService: NotificationService
  ) {}

  async handle(event: UserFollowed): Promise<void> {
    // Cross-context reaction
    await this.notificationService.notifyNewFollower(
      event.followed,
      event.follower
    )
  }
}
```

---

## 6. Proposed Target Architecture

```
src/
├── domain/                     # Core domain logic (no dependencies)
│   ├── identity/
│   │   ├── model/
│   │   │   ├── Account.ts
│   │   │   ├── Pubkey.ts
│   │   │   └── Keypair.ts
│   │   ├── services/
│   │   │   └── SigningService.ts
│   │   └── index.ts
│   ├── social/
│   │   ├── model/
│   │   │   ├── FollowList.ts
│   │   │   ├── MuteList.ts
│   │   │   └── UserProfile.ts
│   │   ├── services/
│   │   │   └── TrustCalculator.ts
│   │   ├── events/
│   │   │   ├── UserFollowed.ts
│   │   │   └── UserMuted.ts
│   │   └── index.ts
│   ├── content/
│   │   ├── model/
│   │   │   ├── Note.ts
│   │   │   ├── Reaction.ts
│   │   │   └── Repost.ts
│   │   ├── services/
│   │   │   └── ContentValidator.ts
│   │   └── index.ts
│   ├── relay/
│   │   ├── model/
│   │   │   ├── RelayUrl.ts
│   │   │   ├── RelaySet.ts
│   │   │   └── RelayList.ts
│   │   ├── services/
│   │   │   └── RelaySelector.ts
│   │   └── index.ts
│   └── shared/
│       ├── EventId.ts
│       ├── Timestamp.ts
│       └── DomainEvent.ts
│
├── application/                # Use cases, orchestration
│   ├── identity/
│   │   └── AccountService.ts
│   ├── social/
│   │   ├── FollowService.ts
│   │   └── MuteService.ts
│   ├── content/
│   │   └── PublishingService.ts
│   └── handlers/
│       └── DomainEventHandlers.ts
│
├── infrastructure/             # External concerns
│   ├── persistence/
│   │   ├── IndexedDbRepository.ts
│   │   └── LocalStorageRepository.ts
│   ├── nostr/
│   │   ├── NostrClient.ts
│   │   └── RelayPool.ts
│   ├── signing/
│   │   ├── NsecSigner.ts
│   │   ├── Nip07Signer.ts
│   │   └── BunkerSigner.ts
│   └── translation/
│       └── TranslationApiClient.ts
│
├── presentation/               # React components
│   ├── providers/              # Thin wrappers around application services
│   ├── components/
│   ├── pages/
│   └── hooks/
│
└── shared/                     # Cross-cutting utilities
    ├── lib/
    └── constants/
```

---

## 7. Migration Strategy

### 7.1 Incremental Approach

1. **Week 1-2:** Create `domain/shared/` with Value Objects (Pubkey, RelayUrl, EventId)
2. **Week 3-4:** Migrate one bounded context (recommend: Social Graph)
3. **Week 5-6:** Add domain services, refactor related providers
4. **Week 7-8:** Introduce repositories for the migrated context
5. **Ongoing:** Repeat for remaining contexts

### 7.2 Coexistence Strategy

During migration, old and new code can coexist:

```typescript
// Adapter to bridge old and new
export function legacyPubkeyToDomain(pubkey: string): Pubkey {
  return Pubkey.fromHex(pubkey)
}

export function domainPubkeyToLegacy(pubkey: Pubkey): string {
  return pubkey.toHex()
}
```

### 7.3 Testing Strategy

- Unit test domain objects in isolation
- Integration test application services
- Keep existing component tests as regression safety

---

## 8. Metrics for Success

| Metric | Current State | Target State |
|--------|---------------|--------------|
| Domain logic in providers | ~60% | <10% |
| Value Objects usage | 0 | 15+ types |
| Explicit aggregates | 0 | 5 aggregates |
| Domain events | 0 (implicit) | 10+ event types |
| Repository interfaces | 0 | 5 repositories |
| Test coverage (domain) | N/A | >80% |

---

## 9. Risks and Mitigations

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Breaking changes during migration | Medium | High | Incremental migration, adapter layer |
| Performance regression | Low | Medium | Benchmark critical paths, optimize lazily |
| Team learning curve | Medium | Medium | Documentation, pair programming |
| Over-engineering | Medium | Medium | YAGNI principle, concrete before abstract |

---

## 10. Conclusion

The Smesh codebase has a solid foundation that can be evolved toward DDD principles. The key recommendations are:

1. **Immediate:** Introduce Value Objects for Pubkey, RelayUrl, EventId
2. **Short-term:** Create rich domain entities with behavior
3. **Medium-term:** Extract domain services from providers
4. **Long-term:** Full bounded context separation with repositories

The Nostr protocol's event-sourced nature is a natural fit for DDD, and the existing type definitions provide a starting point for a rich domain model. The main effort will be moving from an anemic model to entities with behavior, and establishing clear aggregate boundaries.

---

*Generated: December 2024*
*Analysis based on DDD principles from Eric Evans and Vaughn Vernon*
