- 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>
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→ usesFollowListaggregate - ✓
MuteListProvider→ usesMuteListaggregate - ✓
PinnedUsersProvider→ usesPinnedUsersListaggregate - ✓
FavoriteRelaysProvider→ usesFavoriteRelays,RelaySet,RelayUrl - ✓
BookmarksProvider→ usesBookmarkListaggregate - ✓
PinListProvider→ usesPinListaggregate
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
NostrProviderinApp.tsxto 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 handlerseventDispatcher- Global singleton instance- Supports type-specific handlers and catch-all handlers
Event Handlers (src/application/handlers/):
SocialEventHandlers.ts- Handles user follow/unfollow, mute/unmute eventsContentEventHandlers.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)
- Migrate providers to use repositories directly - Replace direct publish calls with repository.save() calls
- Refactor NostrProvider - Consider moving event state management to individual providers using repositories
Medium-term (Next Quarter)
- Create Feed bounded context with proper domain model
- Expand domain event handlers for more cross-context coordination
- Add remaining domain events (NoteCreated, ProfileUpdated, etc.)
Long-term
- Event sourcing consideration - Nostr events are naturally event-sourced
- CQRS pattern for read-heavy feed operations
8. Conclusion
The Smesh codebase has made significant progress toward DDD principles:
- Established domain layer with clear bounded contexts
- Implemented core value objects enabling type-safe domain operations
- Created rich aggregates with encapsulated business rules (8 aggregates)
- Defined domain events for social graph and content operations
- Provided migration path via adapter functions
- Refactored all key providers to use domain aggregates (6 providers now delegate to domain)
- Implemented repository pattern with 8 repository interfaces and 8 implementations for persistence abstraction
- Added event-driven architecture with domain event dispatcher and cross-context handlers
- Completed lib/ utilities migration - all pubkey logic now uses domain Pubkey directly
- 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 hintsMuteList- Public and private mutes with NIP-04 encryptionPinnedUsersList- Pinned users with public/private support (kind 10003)
Repository infrastructure:
- All 8 repository implementations complete with IndexedDB caching and relay publishing
RepositoryProviderprovides dependency-injected repository instances- Repositories handle NIP-04 encryption/decryption for private data
lib/ utilities migration completed:
lib/pubkey.ts- Deprecated functions removed; onlygenerateImageByPubkey(UI utility) remainslib/tag.ts- UsesPubkey.isValidHex()directly from domainlib/nip05.ts- Uses domainPubkeyclass directlylib/event-metadata.ts- Uses domainPubkeyclass directlylib/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