Move product strategy documentation to .product-strategy directory
Some checks failed
CI / build (push) Successful in 21s
CI / integration (push) Failing after 2m1s

Organize all product strategy and domain modeling documentation into a
dedicated .product-strategy directory for better separation from code.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 23:57:11 +01:00
parent 18ea677585
commit 271f5db444
26 changed files with 16521 additions and 0 deletions

View File

@@ -0,0 +1,958 @@
# Domain Model: Namespace Isolation
## Summary
The Namespace Isolation bounded context provides logical boundaries for event visibility and storage. It prevents cross-contamination of events between different logical scopes (tenants, domains, environments) while allowing trusted system components to observe across boundaries when needed. The core invariant is: **events published to one namespace must be invisible to queries from different namespaces**, except through explicit wildcard subscriptions by trusted components.
Unlike opinionated multi-tenancy frameworks, Namespace Isolation treats namespaces as primitives that applications define. The context provides the mechanism (filtering, storage isolation, pattern matching) without enforcing meaning. This aligns with Aether's principle of "Primitives Over Frameworks."
## Invariants
**Invariant: Namespace Boundary Isolation**
- Rule: Events published to namespace X cannot be retrieved by queries to namespace Y (where X ≠ Y), using exact namespace matches
- Scope: Applies across both local EventBus and cross-node NATSEventBus subscriptions
- Why: Multi-scope deployments require data isolation to prevent information leakage and ensure compliance with tenant/domain boundaries
- Implementation:
- EventBus maintains separate subscriber lists per exact namespace (exactSubscribers[namespace])
- JetStreamEventStore creates separate streams per namespace (namespace_events format)
- NATS subjects are namespaced (aether.events.{namespace})
**Invariant: Namespace Name Safety**
- Rule: Namespace names must be safe for use as NATS subject tokens (no wildcards *, >, spaces, or dots)
- Scope: Applies at storage layer (JetStreamEventStore.SaveEvent) and pub/sub layer (EventBus.Subscribe)
- Why: NATS subject tokens have restrictions; invalid names corrupt subject patterns and break filtering
- Implementation: sanitizeSubject() replaces unsafe characters with underscores before using namespace in subjects
**Invariant: Wildcard Subscriptions Bypass Isolation**
- Rule: Subscriptions using wildcard patterns ("*", ">") deliberately receive events from multiple namespaces
- Scope: Applies to EventBus.Subscribe(pattern) and NATSEventBus subscriptions
- Why: Cross-cutting concerns (logging, monitoring, auditing) need visibility across boundaries
- Exception: Only trusted system components should be granted wildcard access
- Implementation: EventBus separates exactSubscribers from wildcardSubscribers; wildcards are documented as security concerns
**Invariant: Subject Pattern Matching Consistency**
- Rule: Namespace patterns must be evaluated consistently using NATS-style token matching across all layers
- Scope: Applies to MatchNamespacePattern(), EventBus pub/sub routing, and NATS subject generation
- Why: Inconsistent matching creates gaps where events are published but not delivered, or vice versa
- Implementation: MatchNamespacePattern() enforces token-based matching (dots separate tokens, "*" matches one token, ">" matches multiple)
## Aggregates
**Note:** This context has NO aggregates. Namespace is not an aggregate because it has no invariants of its own to enforce. It's a primitive value object used by other contexts' aggregates.
Instead, the context defines:
- **Value Objects** for namespace concepts
- **Commands** for registering/configuring namespaces
- **Policies** for enforcing isolation at the storage and pub/sub layers
- **Read Models** for querying events within a namespace
This is intentional: Aether provides primitives, not opinionated domain models.
## Value Objects
### Namespace
A logical boundary identifier. **Immutable, defined by its string value.**
```go
type Namespace string
// Characteristics:
// - No identity beyond the string value
// - Equality is name-based: Namespace("tenant-a") == Namespace("tenant-a")
// - May be hierarchical using dots: "prod.orders", "staging.tenant-123"
// - Application defines meaning (tenant, domain, environment, etc.)
```
**Validation:**
- Should not be empty (semantic meaning required)
- Should not contain unsafe NATS subject characters (sanitized automatically)
- May be hierarchical (dots allowed) for applications using structured naming
**Usage:**
- Passed to EventBus.Subscribe(namespace)
- Passed to NATSEventBus subscriptions
- Configured in JetStreamEventStore.NewJetStreamEventStoreWithNamespace(namespace)
- Embedded in Event.Namespace (not currently in codebase, but design implication)
### SubjectPattern
A pattern for matching one or more namespaces. **Follows NATS conventions.**
```go
type SubjectPattern string
// Examples:
// "tenant-a" - Exact match to one namespace
// "*" - Single wildcard: any namespace with no dots
// "prod.*" - Match "prod.orders", "prod.users", but not "prod.orders.legacy"
// "prod.>" - Match "prod", "prod.orders", "prod.orders.legacy", etc.
// ">" - Global wildcard: all namespaces
```
**Matching Rules:**
- Tokens separated by dots
- "*" matches exactly one token (anything except dots)
- ">" matches one or more tokens (only valid at end)
- Exact strings match exactly
**Security:** Wildcard patterns bypass namespace isolation. Only trusted components should use them.
### NamespaceFilter
Represents a boundary constraint for queries.
```go
type NamespaceFilter {
// Exact namespace to query (no wildcards)
Namespace string
}
```
**Constraint:** Exact namespaces only. When an application explicitly queries a namespace, they should not accidentally get data from multiple namespaces.
## Commands
**Command: DefineNamespace**
- Aggregate: None (Namespace is a value, not an aggregate)
- Input: Namespace name (string)
- Validates:
- Namespace is not empty
- Namespace contains only safe characters (or accepts and sanitizes)
- Invariant enforced: Namespace Name Safety
- Success: Namespace can be used in Subscribe() and CreateEventStore(namespace)
- Failure: Invalid format → reject, never silently sanitize in application layer
**Example (Conceptual):**
```go
// Application defines namespace during initialization
namespace := "tenant-abc" // Validated by application logic
store := store.NewJetStreamEventStoreWithNamespace(natsConn, "events", namespace)
eventBus := aether.NewEventBus()
```
**Command: PublishToNamespace**
- Aggregate: None (action on EventBus, which is not an aggregate)
- Input:
- Namespace: The target namespace (exact match)
- Event: Domain event to publish
- Validates:
- Namespace is defined
- Event is valid (has ID, EventType, ActorID, Version)
- Invariant enforced: Namespace Boundary Isolation (ensures event goes only to exact subscribers)
- Success: Event published to all exact subscribers of this namespace + matching wildcard subscribers
- Failure: Publication error → log and metric
**Example:**
```go
event := &aether.Event{
ID: uuid.New().String(),
EventType: "OrderPlaced",
ActorID: "order-123",
Version: 1,
Data: map[string]interface{}{"total": 100.00},
Timestamp: time.Now(),
}
eventBus.Publish("tenant-abc", event) // Only tenant-abc subscribers receive this
```
**Command: SubscribeToNamespace**
- Aggregate: None
- Input:
- NamespacePattern: Pattern for namespaces to receive (can include wildcards)
- SubscriptionFilter: Optional filter for event types and actors
- Validates:
- Pattern is valid NATS subject format
- If pattern is exact (no wildcards), enforce Namespace Boundary Isolation
- If pattern is wildcard, document as cross-boundary visibility
- Invariant enforced:
- Namespace Boundary Isolation (exact patterns receive only matching namespace events)
- Wildcard Subscriptions Bypass Isolation (wildcard patterns are intentional exceptions)
- Success: Channel created that receives matching events
- Failure: Pattern error → reject, pattern still matches incorrectly → silent miss (events lost)
**Example:**
```go
// Exact subscription - receives only tenant-abc events
ch := eventBus.Subscribe("tenant-abc")
// Wildcard subscription - receives from all prod.* namespaces
// CAUTION: Bypasses namespace isolation for logging/auditing
ch := eventBus.Subscribe("prod.*") // Receives prod.orders, prod.users, etc.
// With filter
filter := &aether.SubscriptionFilter{
EventTypes: []string{"OrderPlaced"},
ActorPattern: "order-*",
}
ch := eventBus.SubscribeWithFilter("tenant-abc", filter)
```
**Command: CreateNamespacedEventStore**
- Aggregate: None
- Input:
- StreamName: Base stream name (e.g., "events")
- Namespace: Logical boundary for storage isolation
- Config: Optional JetStreamConfig (retention, replicas)
- Validates:
- Namespace is not empty (though empty means non-namespaced for backward compat)
- Namespace contains only characters safe for stream names
- Invariant enforced: Namespace Boundary Isolation (separate JetStream streams per namespace)
- Success: JetStreamEventStore created with effective stream name "namespace_streamName"
- Failure: Cannot create stream → return error
**Example:**
```go
// Creates stream: "tenant-abc_events"
store1, _ := store.NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-abc")
// Creates stream: "tenant-def_events"
store2, _ := store.NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-def")
// Events in store1 are completely isolated from store2 (different NATS streams)
```
## Events
**Event: NamespaceCreated (Conceptual - Not Currently Published)**
- Triggered by: Application initialization or registration endpoint
- Aggregate: None (system event, not domain-driven)
- Data: namespace, createdAt, configuredBy
- Consumed by: Auditing, monitoring, provisioning systems
- Note: Currently not part of Aether's event model; application layer concern
**Event: EventPublished**
- Triggered by: PublishToNamespace command
- Aggregate: None (system event)
- Data: The Event itself, namespace it was published to, timestamp
- Consumed by: All subscribers matching the namespace pattern
- Note: In Aether, events are published directly; namespace is routing metadata, not event data
**Event: SubscriptionCreated (Conceptual)**
- Triggered by: SubscribeToNamespace command
- Aggregate: None
- Data: namespacePattern, filter, subscriberID
- Consumed by: Auditing (who is subscribing to what data?)
- Note: Not currently published; application concern for access control
## Policies
**Policy: Namespace Event Routing**
- Trigger: When PublishToNamespace(namespace, event) is called
- Action:
1. Deliver to all exactSubscribers[namespace] matching the event filter
2. Deliver to all wildcardSubscribers where pattern matches namespace
- Context: Enforces Namespace Boundary Isolation while allowing intentional cross-boundary access
- Implementation: EventBus.Publish() → EventBus.deliverToSubscriber() (checks filters, applies AND logic)
**Policy: NATS Subject Namespacing**
- Trigger: When NATSEventBus.Publish(namespace, event) is called
- Action: Publish to NATS subject "aether.events.{namespace}" (exact namespace only, no wildcards)
- Context: Ensures cross-node events respect namespace boundaries
- Implementation:
```go
subject := fmt.Sprintf("aether.events.%s", namespaceID)
neb.nc.Publish(subject, eventMessage)
```
**Policy: NATS Subscription Pattern Replication**
- Trigger: When NATSEventBus.SubscribeWithFilter(pattern, filter) is called
- Action: Create NATS subscription to subject "aether.events.{pattern}" with native NATS wildcard support
- Context: Leverages NATS server-side filtering for efficiency; patterns apply at NATS level
- Implementation:
```go
subject := fmt.Sprintf("aether.events.%s", namespacePattern) // Pattern includes wildcards
neb.nc.Subscribe(subject, handleNATSEvent)
```
- Note: NATS natively supports wildcards, so patterns work at the NATS subject level, not just in EventBus
**Policy: Storage Stream Isolation**
- Trigger: When CreateNamespacedEventStore(namespace) is called
- Action: Create separate JetStream stream with name "{namespace}_{streamName}"
- Context: Provides storage-layer isolation (complete data separation)
- Implementation:
```go
effectiveStreamName := fmt.Sprintf("%s_%s", sanitizeSubject(namespace), streamName)
streamConfig.Name = effectiveStreamName
js.AddStream(streamConfig)
```
- Result: Events in one namespace's stream cannot be read by querying another namespace's store
**Policy: Subject Sanitization**
- Trigger: When namespace or actor ID is used in a NATS subject
- Action: Replace unsafe characters (space, dot, *, >) with underscores
- Context: Ensures all subject components are valid NATS tokens
- Implementation: sanitizeSubject() called before formatting subjects
- Example: "prod.orders" → "prod_orders" when used as stream name prefix
**Policy: Wildcard Warning and Audit**
- Trigger: When IsWildcardPattern(pattern) returns true
- Action: Log warning and record metric, document in comments that this bypasses isolation
- Context: Intentional but requires awareness; prevents accidental exposure
- Implementation:
```go
if IsWildcardPattern(namespacePattern) {
log.Printf("[NATSEventBus] Wildcard subscription: %s", pattern)
metrics.RecordWildcardSubscription(pattern)
}
```
## Read Models
**Read Model: GetEventsInNamespace**
- Purpose: Retrieve all events for an actor within a specific namespace
- Query: GetEvents(store, actorID, fromVersion)
- Data: List[Event] with ID, EventType, ActorID, Version, Data, Timestamp
- Source: JetStreamEventStore (or InMemoryEventStore for tests)
- Updated: Immediately after SaveEvent
- Invariant: Returns events only from specified namespace; cross-namespace queries return empty
- Implementation:
```go
// Queries only the namespace's stream
store := NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-abc")
events, _ := store.GetEvents("order-123", 0) // Only events in tenant-abc_events stream
```
**Read Model: SubscriberCountPerNamespace**
- Purpose: Operational visibility into subscription load
- Query: EventBus.SubscriberCount(namespace) or WildcardSubscriberCount()
- Data: int (count of active subscribers)
- Source: EventBus internal state (exactSubscribers, wildcardSubscribers)
- Updated: When Subscribe/Unsubscribe called
- Usage: Monitoring, scaling decisions
- Implementation: Returns len(exactSubscribers[namespace]) (not including wildcard subscribers)
**Read Model: ActiveNamespacePatterns**
- Purpose: See which patterns are being subscribed to
- Query: Implicit in EventBus.patternSubscribers (NATSEventBus)
- Data: Map[pattern] → count
- Source: NATSEventBus internal tracking
- Updated: When first/last subscriber for pattern arrives
- Usage: Auditing access patterns, understanding data flow
**Read Model: EventReplay (with errors)**
- Purpose: Reconstruct actor state from event stream, with error handling
- Query: GetEventsWithErrors(actorID, fromVersion) → ReplayResult
- Data: {Events: [Event], Errors: [ReplayError]}
- Source: JetStreamEventStore (implements EventStoreWithErrors)
- Updated: On each query (immutable event log)
- Usage: State reconstruction, data quality monitoring
- Implementation: ReplayResult separates successfully parsed events from malformed ones
## Code Analysis: Brownfield (Existing Implementation)
### Current State vs. Intended Domain Model
#### EventBus (eventbus.go)
**Intended:** Value object for namespace patterns; routing policy
**Actual:** Struct with local subscriptions, pattern matching, filtering
**Alignment:** Good
- Exact subscribers and wildcard subscribers are separated (lines 69-71)
- Filtering is applied uniformly (SubscriptionFilter with EventTypes and ActorPattern)
- Pattern matching correctly delegates to MatchNamespacePattern()
- Metrics collection is present
**Found:**
```go
type EventBus struct {
exactSubscribers map[string][]*filteredSubscription // Per namespace
wildcardSubscribers []*filteredSubscription // Cross-namespace
mutex sync.RWMutex
}
```
**Strengths:**
- Clear separation of exact vs. wildcard
- Thread-safe with RWMutex
- Buffered channels prevent blocking (capacity 100)
- Metrics tracking
**Misalignment:**
- Filter matching is implicit in deliverToSubscriber(); document that it's AND logic
- No explicit "namespace boundary enforced here" comment for clarity
---
#### NATSEventBus (nats_eventbus.go)
**Intended:** Extend EventBus with cross-node replication; apply policies at NATS level
**Actual:** Wraps EventBus, creates NATS subscriptions per pattern, deduplicates local events
**Alignment:** Good
- Namespace pattern is used directly as NATS subject suffix (line 89)
- Local events are skipped (line 141: eventMsg.NodeID == neb.nodeID)
- Wildcard subscribers handled specially (lines 148-150)
- Pattern tracking ensures NATS subscription cleanup
**Found:**
```go
subject := fmt.Sprintf("aether.events.%s", namespacePattern) // Patterns include wildcards
neb.nc.Subscribe(subject, func(msg *nats.Msg) {
neb.handleNATSEvent(msg, subscribedPattern)
})
```
**Strengths:**
- Leverages NATS native wildcards correctly
- Deduplication avoids local events being delivered twice
- Reference counting (patternSubscribers) ensures proper cleanup
**Misalignment:**
- No explicit sanitization of namespace before using in subject (relies on EventBus validation)
- handleNATSEvent distinguishes between wildcard and exact patterns (lines 148-154), but this logic could be clearer
---
#### JetStreamEventStore (store/jetstream.go)
**Intended:** Provide storage-layer isolation; enforce namespace safety; implement EventStore interface
**Actual:** JetStream wrapper with namespace prefix, version management, snapshot support
**Alignment:** Excellent
- Namespace config applied to stream name (line 83: "namespace_streamName")
- sanitizeSubject() applied to namespace before formatting subject (line 83)
- Subjects include namespace prefix: "{namespace}_{streamName}.events.{actorType}.{actorID}" (lines 148-151)
- Version concurrency control with mutex
**Found:**
```go
config := JetStreamConfig{Namespace: "tenant-abc"}
effectiveStreamName := fmt.Sprintf("%s_%s",
sanitizeSubject(config.Namespace), // Sanitize namespace
streamName) // "tenant-abc_events"
```
**Strengths:**
- Storage-layer isolation (separate streams per namespace)
- Namespace is optional (backward compatible)
- Sanitization prevents NATS subject injection
- Clear constructor: NewJetStreamEventStoreWithNamespace()
**Misalignment:**
- sanitizeSubject() is private to store package; document that namespaces must be validated by application before reaching storage layer
- No explicit check that namespace-a_events and namespace-b_events are different streams (implicit in NATS stream concept, but should be tested)
---
#### Pattern Matching (pattern.go)
**Intended:** Enforce Subject Pattern Matching Consistency invariant
**Actual:** MatchNamespacePattern(), MatchActorPattern(), IsWildcardPattern()
**Alignment:** Excellent
- Token-based matching with dots as separators (lines 38-41)
- "*" matches one token; ">" matches one or more (lines 54-56, 67-68)
- Special case: ">" alone matches any non-empty namespace (lines 34-35)
**Found:**
```go
func MatchNamespacePattern(pattern, namespace string) bool {
// ">" matches everything when used alone
if pattern == ">" {
return namespace != "" // One or more tokens
}
patternTokens := strings.Split(pattern, ".")
namespaceTokens := strings.Split(namespace, ".")
return matchTokens(patternTokens, namespaceTokens)
}
```
**Strengths:**
- Consistent with NATS token matching
- Recursive token-by-token matching is clear
- IsWildcardPattern() simple and correct
**Misalignment:**
- MatchActorPattern() has two code paths (token-based for dot-separated, simple for non-dot) which is pragmatic but complex (lines 156-197)
- No documentation of the two-path design
---
#### SubscriptionFilter (pattern.go)
**Intended:** Composable filtering with AND logic
**Actual:** EventTypes (OR within list) + ActorPattern, both optional
**Alignment:** Good
- IsEmpty() correctly checks both conditions (lines 111-113)
- Matches() applies AND logic (all filters must pass)
- EventType matching is OR (line 126: "typeMatch := true")
- ActorPattern uses MatchActorPattern()
**Found:**
```go
type SubscriptionFilter struct {
EventTypes []string // OR within this list
ActorPattern string // AND with EventTypes
}
func (f *SubscriptionFilter) Matches(event *Event) bool {
// Check event type filter (OR logic within types)
if len(f.EventTypes) > 0 {
typeMatch := false
for _, et := range f.EventTypes {
if event.EventType == et {
typeMatch = true
break
}
}
if !typeMatch { return false } // AND: must match type
}
// Check actor pattern (AND)
if f.ActorPattern != "" {
if !MatchActorPattern(f.ActorPattern, event.ActorID) {
return false
}
}
return true
}
```
**Strengths:**
- Clear optional filtering
- AND/OR logic is correct and documented
**Misalignment:** None significant.
---
### Namespace Isolation Not Yet in Codebase
The following concepts are designed but not implemented:
1. **Namespace as metadata in Event struct**
- Current: Event has no Namespace field
- Intended: Events should carry their namespace for audit/tracing
- Impact: Currently namespace is transport/routing metadata, not event data
- Refactoring: Add `Namespace string` to Event struct (backward compat via omitempty JSON)
2. **NamespacedEventBus (namespace-aware bus wrapper)**
- Current: Applications manage namespaces externally
- Intended: EventBus could enforce a single namespace per instance
- Impact: Currently developer must pass namespace correctly; bus doesn't validate
- Refactoring: Create wrapper that binds to namespace and prevents accidental cross-namespace access
3. **Namespace validation in application layer**
- Current: No validation before DefineNamespace
- Intended: Application should reject invalid namespace formats
- Impact: Invalid names are silently sanitized at storage layer
- Refactoring: Add namespace.Validate() function and document conventions
---
## Safety Documentation: Wildcard Subscriptions
### Risk: Data Exposure via Wildcard Patterns
Wildcard subscriptions intentionally bypass namespace isolation to enable cross-cutting concerns.
**Scenarios Where Wildcards Are Safe:**
- Trusted logging/auditing system (requires admin access)
- Ops monitoring across all namespaces (requires Ops team authorization)
- Internal distributed tracing (requires service-to-service authentication)
**Scenarios Where Wildcards Are Dangerous:**
- User-facing API that accepts subscription patterns from external clients
- Tenant-specific code that accidentally uses ">" pattern
- Feature flag controlling subscription pattern (default should be exact, not wildcard)
**Mitigation:**
1. **Log and alert** on wildcard subscriptions (NATSEventBus does this)
2. **Code review** any use of "*" or ">" in subscription patterns
3. **Restrict** wildcard subscription to admin/ops code paths
4. **Test** that cross-namespace queries fail without wildcards
5. **Document** in API that patterns are NATS-style and may cross boundaries
### Code Locations and Warnings
| Location | Purpose | Risk |
|----------|---------|------|
| eventbus.go lines 10-16 | EventBroadcaster docs | Documents wildcard behavior |
| eventbus.go lines 63-66 | EventBus struct docs | Notes wildcard as intentional |
| nats_eventbus.go lines 15-20 | NATSEventBus docs | Warns about wildcard bypass |
| pattern.go lines 19-26 | MatchNamespacePattern docs | Security considerations |
| pattern.go lines 101-102 | Subscribe() docs | Explicitly warns about wildcards |
**Good Examples:**
```go
// Exact subscription - safe, enforces boundary
ch := bus.Subscribe("tenant-abc")
// Wildcard for logging - document intent clearly
// CAUTION: Logs events from all namespaces
ch := bus.Subscribe(">") // Comment required!
```
**Bad Examples:**
```go
// Unclear whether wildcard is intentional
pattern := getTenantFilter() // Returns ">" by default? Dangerous!
ch := bus.Subscribe(pattern)
// API accepting patterns from users
func SubscribeToEvents(pattern string) <-chan Event {
return bus.Subscribe(pattern) // User can pass ">" and bypass isolation!
}
```
---
## Refactoring Backlog: Aligning Implementation with Model
### Issue 1: Add Namespace as Event Metadata
**Current:** Events carry no namespace information
**Intended:** Events should record which namespace they belong to for audit trail
**Impact:** Medium - enables better tracing, doesn't break existing code
**Steps:**
1. Add `Namespace string` field to Event struct (JSON: "namespace")
2. Update SaveEvent() to set event.Namespace = namespace
3. Update GetEvents() to filter by namespace (redundant but defensive)
4. Update tests to include namespace in event fixtures
**Acceptance Criteria:**
- Event.Namespace is populated when stored via EventStore
- Replayed events have namespace metadata
- Audit logs include namespace for all events
---
### Issue 2: Validate Namespace Format at Application Layer
**Current:** Invalid namespace names are silently sanitized at storage layer
**Intended:** Application should explicitly validate and reject invalid names
**Impact:** Low - improves error messages, prevents silent data transformation
**Steps:**
1. Create Namespace type alias and validator function
2. Document namespace conventions (no spaces, no dots, etc., or define hierarchical format)
3. Update NewJetStreamEventStoreWithNamespace() to require validated namespace
4. Add examples of valid/invalid namespaces
**Acceptance Criteria:**
- NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant abc") returns error
- NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-abc") succeeds
- Error messages explain what characters are invalid
---
### Issue 3: Create NamespacedEventBus Wrapper
**Current:** EventBus is generic; application must manage namespace per instance
**Intended:** Wrapper enforces single namespace per bus instance
**Impact:** Medium - improves API safety, adds convenience layer
**Steps:**
1. Create NamespacedEventBus(bus *EventBus, namespace string)
2. Override Subscribe() to enforce exact namespace only (reject wildcards at construction time)
3. Override Publish() to validate namespace matches
4. Update examples to show NamespacedEventBus usage
**Acceptance Criteria:**
- NamespacedEventBus.Subscribe() rejects wildcard patterns
- NamespacedEventBus.Publish() verifies namespace matches
- No cross-namespace access possible through this wrapper
---
### Issue 4: Document Namespace Hierarchies
**Current:** Namespace is opaque string; hierarchical naming (prod.orders) is unsupported in docs
**Intended:** Support structured namespaces for domain/environment hierarchies
**Impact:** Low - documentation, no code changes
**Steps:**
1. Document hierarchical naming convention (domain.environment.tenant)
2. Provide examples with pattern matching
3. Show how to query "all production namespaces" vs "single tenant"
4. Warn about dots in namespace names being sanitized at JetStream level
**Acceptance Criteria:**
- Architecture docs explain hierarchical namespace design
- Examples show queries like "prod.*", "prod.orders.>", etc.
- Clear warning about behavior after sanitization (dots become underscores in stream name)
---
### Issue 5: Test Cross-Namespace Isolation
**Current:** namespace_test.go covers stream naming; no integration tests for isolation
**Intended:** Integration tests verify isolation breach is prevented
**Impact:** High - confidence in safety invariant
**Steps:**
1. Create namespace_integration_test.go
2. Test SaveEvent in store1 (tenant-abc_events) is not visible in store2 (tenant-def_events)
3. Test Publish(tenant-abc) is not received by Subscribe(tenant-def)
4. Test Publish(tenant-abc) is received by Subscribe("prod.*") if tenant-abc matches pattern
5. Test wildcard subscriptions deliver correctly across namespaces
**Acceptance Criteria:**
- SaveEvent(store1, event) → GetEvents(store2, event.ActorID) returns empty
- Publish(tenant-a, event) → Subscribe(tenant-b) receives nothing
- Publish(tenant-a, event) → Subscribe(">") receives it
- Pattern matching verified for multi-node setups
---
### Issue 6: Audit Wildcard Subscriptions
**Current:** Wildcard subscriptions are logged but not tracked long-term
**Intended:** Audit trail of who subscribed to what patterns
**Impact:** Low - security audit trail
**Steps:**
1. Create SubscriptionAuditLog interface
2. NATSEventBus calls AuditLog.RecordWildcardSubscription(nodeID, pattern, timestamp)
3. Provide in-memory and persistent implementations
4. Add documentation: "Enable audit logging for compliance"
**Acceptance Criteria:**
- Wildcard subscriptions are recorded with timestamp and node
- Audit log includes pattern, not just count
- Clear documentation on enabling audit logging
---
## Testing Strategy: Namespace Isolation
### Unit Tests (Existing: pattern_test.go, namespace_test.go)
- MatchNamespacePattern() with various dot-separated tokens ✓
- IsWildcardPattern() detection ✓
- sanitizeSubject() character replacement ✓
- SubscriptionFilter AND/OR logic
- EventBus routing to exact vs. wildcard subscribers
### Integration Tests (Needed)
**Test: Storage Layer Isolation**
```go
// Create two namespaced stores
store1 := NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-abc")
store2 := NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-def")
// Save event in store1
event1 := Event{ID: "1", ActorID: "order-123", Version: 1, ...}
store1.SaveEvent(event1)
// Verify store2 cannot see it
events, _ := store2.GetEvents("order-123", 0)
assert.Empty(events) // MUST be empty
```
**Test: Pub/Sub Exact Namespace**
```go
bus := NewEventBus()
ch1 := bus.Subscribe("tenant-abc")
ch2 := bus.Subscribe("tenant-def")
event := Event{ID: "1", ActorID: "order-123", ...}
bus.Publish("tenant-abc", event)
assert.Receives(ch1, event) // MUST receive
assert.Empty(ch2) // MUST NOT receive
```
**Test: Pub/Sub Wildcard Pattern**
```go
bus := NewEventBus()
chExact := bus.Subscribe("tenant-abc")
chWildcard := bus.Subscribe("*") // Intentional bypass
event := Event{ID: "1", ...}
bus.Publish("tenant-abc", event)
assert.Receives(chExact, event) // Exact subscriber
assert.Receives(chWildcard, event) // Wildcard subscriber
```
**Test: Cross-Node NATS Isolation**
```go
neb1 := NewNATSEventBus(natsConn1)
neb2 := NewNATSEventBus(natsConn2) // Different node
ch1 := neb1.Subscribe("tenant-abc")
ch2 := neb2.Subscribe("tenant-def")
event := Event{ID: "1", ...}
neb1.Publish("tenant-abc", event)
// Wait for cross-node delivery
assert.Receives(ch1, event) // Local delivery
assert.Eventually(event in ch1, timeout) // Remote node receives
assert.Empty(ch2) // Different namespace blocked at NATS level
```
**Test: Pattern Matching Consistency**
```go
// Ensure MatchNamespacePattern() matches NATS behavior
tests := []struct {
pattern, namespace string
match bool
}{
("prod.orders", "prod.orders", true),
("prod.*", "prod.orders", true),
("prod.*", "prod.orders.legacy", false),
("prod.>", "prod.orders.legacy", true),
(">", "prod.orders", true),
("*", "prod.orders", false), // "*" doesn't match dots
}
```
---
## Design Decisions and Rationale
### Why Namespace is Not an Aggregate
**Decision:** Namespace is a value object, not an aggregate with invariants.
**Rationale:**
- Namespace has no lifecycle (no creation, deletion, state changes)
- Namespace carries no invariants (e.g., "namespace membership" has no rules)
- Aether philosophy: Primitives, not opinionated frameworks
- Application defines namespace meaning, not Aether
**Alternative Considered:** Make Namespace an aggregate with events like NamespaceCreated, NamespaceDeleted
**Rejected:** Adds complexity without domain benefit; namespace is infrastructure, not business concept
### Why Wildcards Are Allowed Despite Isolation
**Decision:** Wildcard patterns intentionally bypass isolation, documented as security concern.
**Rationale:**
- Cross-cutting concerns need visibility: logging, monitoring, tracing, auditing
- Trusted system components (Ops, admin code) need to observe across boundaries
- Impossible to provide complete isolation while also supporting observability
- Explicit in code (comments, docs) is safer than implicit restriction
**Alternative Considered:** Remove wildcard support, force separate subscriptions per namespace
**Rejected:** Would require application code to manage M*N subscriptions; wildcard is standard NATS pattern
### Why Namespace Is a String, Not a Type
**Decision:** Namespace is string, not a branded type (type Namespace string).
**Rationale:**
- Flexibility: Application defines naming conventions (no framework opinions)
- Go compatibility: String type works with all NATS and storage APIs
- Zero cost abstraction
- Reduced coupling: changing namespace format doesn't require type changes
**Alternative Considered:** Strong type NewNamespace("tenant-abc") to enforce validation
**Rejected:** Primitives over frameworks; validation is application concern
### Why Sanitization Is at Storage Layer, Not Application
**Decision:** JetStreamEventStore.SaveEvent() sanitizes namespace via sanitizeSubject().
**Rationale:**
- Fail-safe: Prevents NATS subject injection even if application doesn't validate
- Single point of truth: All storage goes through same sanitization
- Backward compatible: Existing namespaces still work if stored differently
**Consequence:** Applications see original namespace, but stream names are sanitized
**Example:** Namespace "prod.orders" is stored in stream "prod_orders_events"
**Alternative Considered:** Require application to validate and reject invalid namespaces
**Rejected:** Would require documenting validation rules in multiple places; sanitization is safer
### Why JetStream Streams Are Per-Namespace, Not Per-Actor
**Decision:** Stream name includes namespace prefix: "{namespace}_{streamName}"
**Rationale:**
- Complete storage isolation: Events in one namespace's stream cannot be read from another
- Simpler configuration: Application chooses one namespace per application instance
- Multi-namespace within same store: Not supported (would reduce isolation)
**Trade-off:** Multiple streams consume more JetStream resources but provide stronger isolation
**Alternative Considered:** Single stream with namespace as message metadata filter
**Rejected:** Application-side filtering is not as secure; NATS stream-level isolation is stronger
---
## Alignment with Aether Vision
### Primitives Over Frameworks
Namespace Isolation provides:
- **Namespace** (value): String identifier for boundaries
- **SubscriptionFilter** (value): Optional filtering rules
- **EventBus.Subscribe(pattern)** (primitive): Core pub/sub with pattern matching
- **EventStore.SaveEvent(event)** (primitive): Core persistence with namespace isolation built-in
**Does NOT provide:**
- Opinionated multi-tenancy framework
- Tenant lifecycle management
- Namespace quota enforcement
- Permission-based namespace access control
Applications build these on top of primitives.
### NATS-Native
- **Subject patterns:** Uses NATS native "*" and ">" wildcards
- **JetStream streams:** Separate stream per namespace leverages NATS architecture
- **Node-to-node:** NATSEventBus publishes with namespace in subject prefix
- **No abstraction layer:** Direct use of NATS concepts, not hidden
### Resource Conscious
- **Minimal overhead:** Namespace is just a string; filtering is O(subscriber count)
- **Efficient patterns:** NATS server-side filtering with subjects
- **No namespace registry:** Namespaces are defined implicitly by use
- **Optional:** Namespace is opt-in; backward compatible with no namespace
### Events as Complete History
- **Immutable per namespace:** Events in namespace X cannot be modified
- **Complete audit trail:** All events visible via replay (filtered by namespace)
- **Namespace metadata:** Events could (should) record their namespace
- **No deletion:** Namespaces don't have "cleanup" commands; events persist per retention policy
---
## Recommendations for Implementation
### Priority 1: Storage-Level Isolation Tests
**Why:** Highest risk; must ensure separate streams truly isolate data
**Effort:** 1-2 days
**Impact:** Confidence in safety invariant
### Priority 2: Add Namespace to Event Metadata
**Why:** Needed for audit trails; enables namespace field in logs
**Effort:** 2-3 days
**Impact:** Better observability
### Priority 3: NamespacedEventBus Wrapper
**Why:** Improves API safety; prevents accidental wildcard subscriptions
**Effort:** 2-3 days
**Impact:** Easier to use safely
### Priority 4: Document Namespace Hierarchies
**Why:** Required for multi-scope deployments; clarifies intended patterns
**Effort:** 1 day
**Impact:** Clear guidance for applications
### Priority 5: Audit Logging for Wildcards
**Why:** Security audit trail; helps detect unauthorized access patterns
**Effort:** 3-4 days
**Impact:** Compliance and monitoring
---
## References
- **Code Location:** `/Users/hugo.nijhuis/src/github/flowmade-one/aether/`
- **Key Files:**
- eventbus.go (lines 10-268)
- nats_eventbus.go (lines 1-231)
- pattern.go (lines 1-197)
- store/jetstream.go (lines 1-382)
- store/namespace_test.go (lines 1-125)
- **Vision Alignment:** /aether/vision.md (Primitives Over Frameworks, NATS-Native)
- **CLAUDE.md:** /aether/CLAUDE.md (namespace isolation context from bounded context map)