Files
smesh/DDD_ANALYSIS.md
woikos 4c3e8d5cc7 Release v0.3.1
- Feed bounded context with DDD implementation (Phases 1-5)
- Domain event handlers for cross-context coordination
- Fix Blossom media upload setting persistence
- Fix wallet connection persistence on page reload
- New branding assets and icons
- Vitest testing infrastructure with 151 domain model tests
- Help page scaffolding
- Keyboard navigation provider

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 07:29:07 +01:00

28 KiB

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 covers the current domain layer implementation, evaluates progress against DDD principles, and provides recommendations for continued evolution.

Current Status (January 2026):

  • Domain layer established with explicit bounded contexts
  • Core value objects implemented (Pubkey, RelayUrl, EventId, Timestamp)
  • Rich aggregates created (FollowList, MuteList, PinnedUsersList, RelaySet, RelayList, FavoriteRelays, BookmarkList, PinList)
  • Domain events defined for social context
  • Application services layer started (PublishingService, RelaySelector)
  • Migration adapters enable incremental adoption
  • All key providers refactored to use domain aggregates:
    • FollowListProvider → FollowList aggregate
    • MuteListProvider → MuteList aggregate
    • PinnedUsersProvider → PinnedUsersList aggregate
    • FavoriteRelaysProvider → FavoriteRelays, RelaySet, RelayUrl
    • BookmarksProvider → BookmarkList aggregate
    • PinListProvider → PinList aggregate

Remaining Work:

  • Integrate repositories into providers (replace direct service calls)
  • Create Feed bounded context with proper domain model

1. Domain Layer Architecture

1.1 Directory Structure

src/
├── domain/                     # Core domain logic
│   ├── index.ts               # Domain layer exports
│   ├── shared/                # Shared Kernel
│   │   ├── value-objects/
│   │   │   ├── Pubkey.ts      ✓ Implemented
│   │   │   ├── RelayUrl.ts    ✓ Implemented
│   │   │   ├── EventId.ts     ✓ Implemented
│   │   │   └── Timestamp.ts   ✓ Implemented
│   │   ├── errors.ts          ✓ Domain errors
│   │   └── adapters.ts        ✓ Migration helpers
│   ├── identity/              # Identity Bounded Context
│   │   ├── Account.ts         ✓ Entity
│   │   ├── SignerType.ts      ✓ Value Object
│   │   ├── errors.ts          ✓ Domain errors
│   │   └── adapters.ts        ✓ Migration helpers
│   ├── social/                # Social Graph Bounded Context
│   │   ├── FollowList.ts      ✓ Aggregate
│   │   ├── MuteList.ts        ✓ Aggregate
│   │   ├── PinnedUsersList.ts ✓ Aggregate
│   │   ├── events.ts          ✓ Domain Events
│   │   ├── repositories.ts    ✓ Repository interfaces
│   │   ├── errors.ts          ✓ Domain errors
│   │   └── adapters.ts        ✓ Migration helpers
│   ├── relay/                 # Relay Bounded Context
│   │   ├── RelaySet.ts        ✓ Aggregate
│   │   ├── RelayList.ts       ✓ Aggregate
│   │   ├── FavoriteRelays.ts  ✓ Aggregate
│   │   ├── repositories.ts    ✓ Repository interfaces
│   │   ├── errors.ts          ✓ Domain errors
│   │   └── adapters.ts        ✓ Migration helpers
│   └── content/               # Content Bounded Context
│       ├── Note.ts            ✓ Entity
│       ├── Reaction.ts        ✓ Value Object
│       ├── Repost.ts          ✓ Value Object
│       ├── BookmarkList.ts    ✓ Aggregate
│       ├── PinList.ts         ✓ Aggregate
│       ├── errors.ts          ✓ Domain errors
│       └── adapters.ts        ✓ Migration helpers
│
├── application/               # Application Services
│   ├── PublishingService.ts   ✓ Domain Service
│   └── RelaySelector.ts       ✓ Domain Service
│
├── infrastructure/            # Infrastructure Layer
│   └── persistence/           # Repository implementations
│       ├── FollowListRepositoryImpl.ts       ✓
│       ├── MuteListRepositoryImpl.ts         ✓
│       ├── PinnedUsersListRepositoryImpl.ts  ✓
│       ├── RelayListRepositoryImpl.ts        ✓
│       ├── RelaySetRepositoryImpl.ts         ✓
│       ├── FavoriteRelaysRepositoryImpl.ts   ✓
│       ├── BookmarkListRepositoryImpl.ts     ✓
│       └── PinListRepositoryImpl.ts          ✓
│
├── providers/                 # React Context (presentation layer)
├── services/                  # Infrastructure services
├── components/                # UI components
└── lib/                       # Legacy utilities (being migrated)

1.2 Bounded Contexts

┌─────────────────────────────────────────────────────────────────────┐
│                         CONTEXT MAP                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│    ┌───────────────┐                      ┌─────────────────┐       │
│    │   Identity    │     Partnership      │  Social Graph   │       │
│    │   Context     │◄───────────────────►│    Context      │       │
│    │               │                      │                 │       │
│    │ • Account     │                      │ • FollowList    │       │
│    │ • SignerType  │                      │ • MuteList      │       │
│    │               │                      │ • PinnedUsersList│      │
│    └───────┬───────┘                      └───────┬─────────┘       │
│            │                                      │                  │
│            │ Customer/Supplier                    │ Partnership     │
│            ▼                                      ▼                  │
│    ┌───────────────┐                      ┌───────────────┐         │
│    │    Content    │                      │     Feed      │         │
│    │    Context    │                      │    Context    │         │
│    │               │                      │               │         │
│    │ • Note        │                      │ (Providers)   │         │
│    │ • Reaction    │                      │               │         │
│    │ • Repost      │                      │               │         │
│    │ • BookmarkList│                      │               │         │
│    │ • PinList     │                      │               │         │
│    └───────────────┘                      └───────┬───────┘         │
│            │                                      │                  │
│            └──────────────┬───────────────────────┘                 │
│                           ▼                                          │
│                   ┌───────────────┐                                 │
│                   │     Relay     │                                 │
│                   │    Context    │                                 │
│                   │               │                                 │
│                   │ • RelayUrl    │                                 │
│                   │ • RelaySet    │                                 │
│                   │ • RelayList   │                                 │
│                   └───────────────┘                                 │
│                                                                      │
│    ┌───────────────────────────────────────────────────────────┐   │
│    │                    SHARED KERNEL                           │   │
│    │  Pubkey | EventId | Timestamp | RelayUrl | DomainError    │   │
│    └───────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2. Implementation Details

2.1 Value Objects (Shared Kernel)

All value objects follow DDD principles: immutable, self-validating, equality by value.

Value Object Status Key Features
Pubkey ✓ Complete Hex/npub/nprofile parsing, validation, formatting
RelayUrl ✓ Complete URL normalization, WebSocket validation, secure/onion detection
EventId ✓ Complete Hex/nevent parsing, validation
Timestamp ✓ Complete Unix timestamp wrapper, date conversion

Example: Pubkey Value Object

// Self-validating factory methods
const pubkey = Pubkey.fromHex('abc123...')        // throws InvalidPubkeyError
const pubkey = Pubkey.fromNpub('npub1...')        // throws InvalidPubkeyError
const pubkey = Pubkey.tryFromString('...')        // returns null on invalid

// Immutable with rich behavior
pubkey.hex          // "abc123..."
pubkey.npub         // "npub1..."
pubkey.formatted    // "abc123...xyz4"
pubkey.formatNpub(12)  // "npub1abc...xyz"

// Equality by value
pubkey1.equals(pubkey2)  // true if same hex

2.2 Aggregates

FollowList Aggregate (Social Context)

class FollowList {
  // Factory methods
  static empty(owner: Pubkey): FollowList
  static fromEvent(event: Event): FollowList

  // Invariants enforced
  follow(pubkey): FollowListChange   // throws CannotFollowSelfError
  unfollow(pubkey): FollowListChange

  // Rich behavior
  isFollowing(pubkey): boolean
  getFollowing(): Pubkey[]
  getEntries(): FollowEntry[]
  setPetname(pubkey, name): boolean
  merge(other: FollowList): void

  // Persistence support
  toTags(): string[][]
  toDraftEvent(): DraftEvent
}

MuteList Aggregate (Social Context)

class MuteList {
  // Factory methods
  static empty(owner: Pubkey): MuteList
  static fromEvent(event: Event, decryptedPrivateTags: string[][]): MuteList

  // Invariants enforced
  mutePublicly(pubkey): MuteListChange   // throws CannotMuteSelfError
  mutePrivately(pubkey): MuteListChange
  unmute(pubkey): MuteListChange

  // Rich behavior
  isMuted(pubkey): boolean
  getMuteVisibility(pubkey): MuteVisibility | null
  switchToPrivate(pubkey): MuteListChange
  switchToPublic(pubkey): MuteListChange

  // Persistence support
  toPublicTags(): string[][]
  toPrivateTags(): string[][]  // For NIP-04 encryption
  toDraftEvent(encryptedContent): DraftEvent
}

PinnedUsersList Aggregate (Social Context)

class PinnedUsersList {
  // Factory methods
  static empty(owner: Pubkey): PinnedUsersList
  static fromEvent(event: Event): PinnedUsersList

  // Invariants enforced
  pin(pubkey): PinnedUsersListChange    // throws Error if pinning self
  unpin(pubkey): PinnedUsersListChange

  // Rich behavior
  isPinned(pubkey): boolean
  getPinnedPubkeys(): Pubkey[]
  getEntries(): PinnedUserEntry[]
  getPublicEntries(): PinnedUserEntry[]
  getPrivateEntries(): PinnedUserEntry[]

  // Private pins support (NIP-04 encrypted)
  setPrivatePins(privateTags: string[][]): void
  setEncryptedContent(content: string): void

  // Persistence support
  toTags(): string[][]           // Public pins
  toPrivateTags(): string[][]    // For NIP-04 encryption
  toDraftEvent(): DraftEvent
}

RelaySet Aggregate (Relay Context)

class RelaySet {
  // Factory methods
  static create(name: string, id?: string): RelaySet
  static createWithRelays(name: string, relayUrls: string[], id?: string): RelaySet
  static fromEvent(event: Event): RelaySet

  // Invariants enforced (valid URLs, no duplicates)
  addRelay(relay: RelayUrl): RelaySetChange
  removeRelay(relay: RelayUrl): RelaySetChange

  // Rich behavior
  rename(newName: string): void
  hasRelay(relay: RelayUrl): boolean
  getRelays(): RelayUrl[]

  // Persistence support
  toTags(): string[][]
  toDraftEvent(): DraftEvent
}

BookmarkList Aggregate (Content Context)

class BookmarkList {
  // Factory methods
  static empty(owner: Pubkey): BookmarkList
  static fromEvent(event: Event): BookmarkList
  static tryFromEvent(event: Event | null): BookmarkList | null

  // Rich behavior
  addFromEvent(event: Event): BookmarkListChange
  addEvent(eventId: EventId, pubkey?: Pubkey): BookmarkListChange
  addReplaceable(coordinate: string): BookmarkListChange
  remove(idOrCoordinate: string): BookmarkListChange
  removeFromEvent(event: Event): BookmarkListChange

  // Query methods
  isBookmarked(idOrCoordinate: string): boolean
  hasEventId(eventId: string): boolean
  hasCoordinate(coordinate: string): boolean
  getEntries(): BookmarkEntry[]
  getEventIds(): string[]
  getReplaceableCoordinates(): string[]

  // Persistence support
  toTags(): string[][]
  toDraftEvent(): DraftEvent
}

PinList Aggregate (Content Context)

class PinList {
  // Factory methods
  static empty(owner: Pubkey): PinList
  static fromEvent(event: Event): PinList
  static tryFromEvent(event: Event | null): PinList | null

  // Invariants enforced
  pin(event: Event): PinListChange    // throws CannotPinOthersContentError, CanOnlyPinNotesError
  unpin(eventId: string): PinListChange
  unpinEvent(event: Event): PinListChange

  // Query methods
  isPinned(eventId: string): boolean
  getEntries(): PinEntry[]
  getEventIds(): string[]
  getEventIdSet(): Set<string>
  get isFull(): boolean  // max 5 pins

  // Persistence support
  toTags(): string[][]
  toDraftEvent(): DraftEvent
}

2.3 Entities

Note Entity (Content Context)

class Note {
  // Factory method
  static fromEvent(event: Event): Note
  static tryFromEvent(event: Event | null): Note | null

  // Identity
  get id(): EventId
  get author(): Pubkey

  // Rich behavior
  get noteType(): 'root' | 'reply' | 'quote'
  get isRoot(): boolean
  get isReply(): boolean
  get mentions(): NoteMention[]
  get references(): NoteReference[]
  get hashtags(): string[]
  get hasContentWarning(): boolean
  get contentWarning(): string | undefined

  // Query methods
  mentionsUser(pubkey: Pubkey): boolean
  referencesNote(eventId: EventId): boolean
  hasHashtag(hashtag: string): boolean
}

Account Entity (Identity Context)

class Account {
  // Factory methods
  static create(pubkey: Pubkey, signerType: SignerType, credentials?: AccountCredentials): Account
  static fromRaw(pubkeyHex: string, signerTypeValue: string, credentials?: AccountCredentials): Account
  static fromLegacy(legacy: TAccount): Account | null

  // Identity and behavior
  get id(): string  // pubkey:signerType
  get pubkey(): Pubkey
  get signerType(): SignerType
  get canSign(): boolean
  get isViewOnly(): boolean
  equals(other: Account): boolean
  hasSamePubkey(other: Account): boolean

  // Persistence support
  toLegacy(): TAccount
  toPointer(): { pubkey: string; signerType: string }
}

2.4 Domain Events

// Base event
abstract class DomainEvent {
  readonly occurredAt: Timestamp
  abstract get eventType(): string
}

// Social context events
class UserFollowed extends DomainEvent {
  eventType = 'social.user_followed'
  constructor(actor: Pubkey, followed: Pubkey, relayHint?: string, petname?: string)
}

class UserUnfollowed extends DomainEvent {
  eventType = 'social.user_unfollowed'
  constructor(actor: Pubkey, unfollowed: Pubkey)
}

class UserMuted extends DomainEvent {
  eventType = 'social.user_muted'
  constructor(actor: Pubkey, muted: Pubkey, visibility: MuteVisibility)
}

class UserUnmuted extends DomainEvent {
  eventType = 'social.user_unmuted'
  constructor(actor: Pubkey, unmuted: Pubkey)
}

class MuteVisibilityChanged extends DomainEvent {
  eventType = 'social.mute_visibility_changed'
  constructor(actor: Pubkey, target: Pubkey, from: MuteVisibility, to: MuteVisibility)
}

class FollowListPublished extends DomainEvent
class MuteListPublished extends DomainEvent

2.5 Application Services

// PublishingService - creates draft events
class PublishingService {
  createNoteDraft(content: string, options: PublishNoteOptions): DraftEvent
  createReactionDraft(targetEventId: string, targetPubkey: string, targetKind: number, emoji: string): DraftEvent
  createRepostDraft(targetEventId: string, targetPubkey: string, embeddedContent?: string): DraftEvent
  createFollowListDraft(followList: FollowList): DraftEvent
  createMuteListDraft(muteList: MuteList, encryptedPrivateMutes: string): DraftEvent
  createRelayListDraft(relayList: RelayList): DraftEvent
  extractMentionsFromContent(content: string): Pubkey[]
  extractHashtagsFromContent(content: string): string[]
}

// RelaySelector - determines relays for publishing
class RelaySelector {
  selectWriteRelays(userRelayList: RelayList): RelayUrl[]
  selectReadRelays(userRelayList: RelayList): RelayUrl[]
  selectForUser(pubkey: Pubkey, userRelayList: RelayList): RelayUrl[]
  mergeRelays(sources: RelayUrl[][]): RelayUrl[]
}

2.6 Migration Adapters

Each bounded context provides adapters for incremental migration from legacy code:

// Pubkey adapters
toPubkey(hex: string): Pubkey           // Create from hex
tryToPubkey(hex: string): Pubkey | null // Safe creation
fromPubkey(pubkey: Pubkey): string      // Extract hex
toPubkeys(hexArray: string[]): Pubkey[] // Bulk conversion
fromPubkeys(pubkeys: Pubkey[]): string[] // Bulk extraction
createPubkeySet(hexArray: string[]): Set<string> // For fast lookups

// FollowList adapters
toFollowList(owner: string, hexArray: string[]): FollowList
fromFollowListToHexSet(followList: FollowList): Set<string>
isFollowingHex(followList: FollowList, hex: string): boolean
followByHex(followList: FollowList, hex: string): FollowListChange

// MuteList adapters
toMuteList(owner: string, publicHexes: string[], privateHexes: string[]): MuteList
fromMuteListToHexSet(muteList: MuteList): Set<string>
isMutedHex(muteList: MuteList, hex: string): boolean
createMuteFilter(muteList: MuteList): (hex: string) => boolean

// PinnedUsersList adapters
toPinnedUsersList(event: Event, decryptedPrivateTags?: string[][]): PinnedUsersList
fromPinnedUsersListToHexSet(list: PinnedUsersList): Set<string>
isPinnedHex(list: PinnedUsersList, hex: string): boolean
pinByHex(list: PinnedUsersList, hex: string): boolean
unpinByHex(list: PinnedUsersList, hex: string): boolean
createPinnedFilter(list: PinnedUsersList): (hex: string) => boolean

3. Anti-Pattern Status

Anti-Pattern Original Status Current Status Progress
Anemic Domain Model High severity Mostly resolved Aggregates have behavior; key providers now delegate to domain
Smart UI Medium severity Partially resolved Domain logic moving to aggregates; some UI components still have logic
Database-Driven Design Low severity Resolved Domain model independent of storage
Missing Aggregate Boundaries Medium severity Resolved Clear aggregates with invariants
Leaky Abstractions Medium severity Mostly resolved Domain layer has no infrastructure deps; providers act as thin orchestration layer

4. Remaining Work

4.1 Phase 3: Provider Integration (Complete)

All key providers have been refactored to use domain aggregates:

Completed:

  • FollowListProvider → uses FollowList aggregate
  • MuteListProvider → uses MuteList aggregate
  • PinnedUsersProvider → uses PinnedUsersList aggregate
  • FavoriteRelaysProvider → uses FavoriteRelays, RelaySet, RelayUrl
  • BookmarksProvider → uses BookmarkList aggregate
  • PinListProvider → uses PinList aggregate

Example: Refactored Pattern

// Before: Logic in provider
const addBookmark = async (event: Event) => {
  const currentTags = bookmarkListEvent?.tags || []
  if (currentTags.some(tag => tag[0] === 'e' && tag[1] === event.id)) return
  const newTags = [...currentTags, buildETag(event.id, event.pubkey)]
  // manual tag construction...
}

// After: Delegate to domain
const addBookmark = async (event: Event) => {
  const bookmarkList = tryToBookmarkList(bookmarkListEvent) ?? BookmarkList.empty(owner)
  const change = bookmarkList.addFromEvent(event)
  if (change.type === 'no_change') return
  const draftEvent = bookmarkList.toDraftEvent()
  await publish(draftEvent)
}

4.2 Phase 4: Repository Implementation (Complete)

Repository implementations created in src/infrastructure/persistence/:

Implemented Repositories:

  • FollowListRepositoryImpl - Social context
  • MuteListRepositoryImpl - Social context (with NIP-04 encryption support)
  • PinnedUsersListRepositoryImpl - Social context (with NIP-04 encryption support)
  • RelayListRepositoryImpl - Relay context
  • RelaySetRepositoryImpl - Relay context
  • FavoriteRelaysRepositoryImpl - Relay context
  • BookmarkListRepositoryImpl - Content context
  • PinListRepositoryImpl - Content context

Dependency Injection:

  • RepositoryProvider - Provides repository instances to React components via context
  • Located in src/providers/RepositoryProvider.tsx
  • Nested after NostrProvider in App.tsx to access publish and encryption functions

Usage Pattern:

// Create repository with dependencies
const followListRepo = new FollowListRepositoryImpl({ publish })

// Find aggregate
const followList = await followListRepo.findByOwner(pubkey)

// Save aggregate (publishes to relays and updates cache)
await followListRepo.save(followList)

4.3 Phase 5: Event-Driven Architecture (Complete)

Domain event infrastructure implemented in src/domain/shared/events.ts:

Event Dispatcher:

  • SimpleEventDispatcher - In-memory event bus with type-safe handlers
  • eventDispatcher - Global singleton instance
  • Supports type-specific handlers and catch-all handlers

Event Handlers (src/application/handlers/):

  • SocialEventHandlers.ts - Handles user follow/unfollow, mute/unmute events
  • ContentEventHandlers.ts - Handles bookmark, pin, reaction events

Event Types:

  • Social: UserFollowed, UserUnfollowed, UserMuted, UserUnmuted, MuteVisibilityChanged, FollowListPublished, MuteListPublished
  • Content: EventBookmarked, EventUnbookmarked, NotePinned, NoteUnpinned, PinsLimitExceeded, ReactionAdded, ContentReposted

Usage Pattern:

// Dispatch events from providers
await eventDispatcher.dispatch(
  new EventBookmarked(ownerPubkey, eventId, 'event')
)

// Register handlers
eventDispatcher.on('content.event_bookmarked', async (event) => {
  console.log('Bookmarked:', event.bookmarkedEventId)
})

5. Metrics

Metric December 2024 January 2026 Target
Value Objects 0 4 types 4+ ✓
Explicit Aggregates 0 8 aggregates 5+ ✓
Domain Events 0 7 event types 10+
Domain Entities 0 3 entities 5
Repository Interfaces 0 8 interfaces 5+ ✓
Repository Implementations 0 8 implementations 5+ ✓
Application Services 0 2 services 4
Providers Using Domain 0 6 providers 5+ ✓
Domain Logic in Providers ~60% ~10% <10% ✓

6. Implementation Checklist

  • Ubiquitous language documented (Nostr terminology)
  • Bounded contexts identified (Identity, Social, Content, Relay, Feed)
  • Context map documented with relationships
  • Value objects for primitives (Pubkey, RelayUrl, EventId, Timestamp)
  • Aggregates with clear invariants (FollowList, MuteList, PinnedUsersList, RelaySet, RelayList, FavoriteRelays, BookmarkList, PinList)
  • Entities with behavior (Account, Note, Reaction, Repost)
  • Domain events for state changes (Social context events)
  • Domain errors hierarchy (InvalidPubkeyError, CannotFollowSelfError, CannotPinOthersContentError, etc.)
  • Migration adapters for incremental adoption
  • Key providers refactored to use domain aggregates (FollowListProvider, MuteListProvider, PinnedUsersProvider, FavoriteRelaysProvider, BookmarksProvider, PinListProvider)
  • Repositories abstract persistence (7 interfaces with IndexedDB + Relay implementations)
  • Event handlers for cross-context communication (SocialEventHandlers, ContentEventHandlers)
  • Migration from legacy lib/ utilities (completed - all pubkey logic now uses domain Pubkey directly)

7. Recommendations

Short-term (Next Sprint)

  1. Migrate providers to use repositories directly - Replace direct publish calls with repository.save() calls
  2. Refactor NostrProvider - Consider moving event state management to individual providers using repositories

Medium-term (Next Quarter)

  1. Create Feed bounded context with proper domain model
  2. Expand domain event handlers for more cross-context coordination
  3. Add remaining domain events (NoteCreated, ProfileUpdated, etc.)

Long-term

  1. Event sourcing consideration - Nostr events are naturally event-sourced
  2. CQRS pattern for read-heavy feed operations

8. Conclusion

The Smesh codebase has made significant progress toward DDD principles:

  1. Established domain layer with clear bounded contexts
  2. Implemented core value objects enabling type-safe domain operations
  3. Created rich aggregates with encapsulated business rules (8 aggregates)
  4. Defined domain events for social graph and content operations
  5. Provided migration path via adapter functions
  6. Refactored all key providers to use domain aggregates (6 providers now delegate to domain)
  7. Implemented repository pattern with 8 repository interfaces and 8 implementations for persistence abstraction
  8. Added event-driven architecture with domain event dispatcher and cross-context handlers
  9. Completed lib/ utilities migration - all pubkey logic now uses domain Pubkey directly
  10. Created RepositoryProvider for dependency injection of repositories into React components

The anemic domain model anti-pattern has been resolved. Providers now act as thin orchestration layers that delegate business logic to domain aggregates. Domain events are dispatched when aggregates change, enabling cross-context communication.

Social context aggregates:

  • FollowList - Following relationships with petnames and relay hints
  • MuteList - Public and private mutes with NIP-04 encryption
  • PinnedUsersList - Pinned users with public/private support (kind 10003)

Repository infrastructure:

  • All 8 repository implementations complete with IndexedDB caching and relay publishing
  • RepositoryProvider provides dependency-injected repository instances
  • Repositories handle NIP-04 encryption/decryption for private data

lib/ utilities migration completed:

  • lib/pubkey.ts - Deprecated functions removed; only generateImageByPubkey (UI utility) remains
  • lib/tag.ts - Uses Pubkey.isValidHex() directly from domain
  • lib/nip05.ts - Uses domain Pubkey class directly
  • lib/event-metadata.ts - Uses domain Pubkey class directly
  • lib/relay.ts, lib/event.ts - Infrastructure utilities (appropriate to keep)
  • All services, providers, components, and hooks now import directly from @/domain

The next priorities are:

  • Migrating providers to use repositories directly for persistence
  • Creating Feed bounded context with proper domain model

Updated: January 2026 Analysis based on DDD principles from Eric Evans and Vaughn Vernon