Files
aether/.product-strategy/NAMESPACE_ISOLATION_SUMMARY.md
Hugo Nijhuis 271f5db444
Some checks failed
CI / build (push) Successful in 21s
CI / integration (push) Failing after 2m1s
Move product strategy documentation to .product-strategy directory
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>
2026-01-12 23:57:20 +01:00

15 KiB

Namespace Isolation: Summary and Implementation Gaps

Executive Summary

The Namespace Isolation bounded context provides logical boundaries for event visibility and storage in Aether. The implementation is mostly complete with strong primitives for exact namespace matching and NATS-native wildcard support. The main gaps are metadata tracking (namespace field on events) and application-layer validation (explicit namespace format checking).

Status: Core invariants are enforced; refactoring needed for observability and safety.


Core Invariants: Enforcement Status

Invariant Status Evidence
Namespace Boundary Isolation ✓ Enforced EventBus separates exactSubscribers by namespace; JetStream streams are per-namespace
Namespace Name Safety ✓ Enforced sanitizeSubject() applied at storage layer; NATS subject injection prevented
Wildcard Subscriptions Bypass Isolation ✓ Documented Explicit in code comments and docstrings (eventbus.go lines 15-16, 101-102)
Subject Pattern Matching Consistency ✓ Enforced MatchNamespacePattern() consistent with NATS token-based matching

Implementation Alignment

EventBus: Exact and Wildcard Routing

Architecture:

EventBus
├── exactSubscribers: map[namespace] → [subscriber]     (isolation enforced)
├── wildcardSubscribers: [subscriber]                   (cross-boundary)
└── Publish(namespace) → delivers to both groups (with filter matching)

Lines: eventbus.go 69-71, 131-137, 186-205 Invariant Enforced: Namespace Boundary Isolation

Strengths:

  • Exact subscribers only receive from their namespace
  • Wildcard subscribers are separate list (intentional, documented)
  • Filter matching is AND logic (events must pass all filters)

Gap Identified:

  • No explicit comment explaining separation strategy
  • Could document: "exactSubscribers enforce isolation; wildcardSubscribers are intentional exceptions"

NATSEventBus: Cross-Node Replication

Architecture:

NATSEventBus (extends EventBus)
├── Subscribe(pattern) → creates NATS subject "aether.events.{pattern}"
├── Publish(namespace, event) → publishes to "aether.events.{namespace}"
└── handleNATSEvent() → deduplicates local events, delivers to wildcard subscribers

Lines: nats_eventbus.go 63-111, 186-211 Invariant Enforced: Namespace Boundary Isolation (at NATS subject level)

Strengths:

  • NATS subjects include namespace as prefix
  • Local node events are deduplicated (not re-delivered)
  • Wildcard patterns work via NATS native subject matching
  • Pattern tracking prevents duplicate NATS subscriptions

Gap Identified:

  • No sanitization of namespace before formatting NATS subject (relies on EventBus callers to pass valid namespaces)
  • Mitigation: JetStreamEventStore sanitizes at storage layer (defense in depth)

JetStreamEventStore: Storage-Layer Isolation

Architecture:

JetStreamEventStore
├── Namespace: "tenant-abc" → Stream: "tenant_abc_events"
├── Subject: "{namespace}_{streamName}.events.{actorType}.{actorID}"
└── SaveEvent() → publishes only to namespaced stream

Lines: store/jetstream.go 66-106, 148-157 Invariants Enforced: Namespace Boundary Isolation, Namespace Name Safety

Strengths:

  • Separate JetStream streams per namespace (complete storage isolation)
  • sanitizeSubject() applied to namespace before stream name (lines 83)
  • GetEvents() queries only the configured stream (cannot see other namespaces)
  • NewJetStreamEventStoreWithNamespace() has clear, explicit constructor

Gaps Identified:

  1. No namespace field in Event struct

    • Current: namespace is transport metadata, not event data
    • Implication: Replayed events don't carry their namespace
    • Risk: Difficult to audit which namespace an event belonged to
    • Refactoring: Add Namespace string field to Event (JSON: "namespace", omitempty)
  2. No namespace validation before CreateNamespacedEventStore

    • Current: Invalid names are silently sanitized
    • Example: NewJetStreamEventStoreWithNamespace(conn, "events", "tenant.abc") succeeds, but stream becomes "tenant_abc_events"
    • Implication: Behavior change is silent; developer thinks they're using "tenant.abc" but storage uses "tenant_abc"
    • Refactoring: Add Namespace.Validate() function; reject invalid names with clear errors

Pattern Matching: Token-Based Consistency

Implementation: pattern.go 27-77 Invariant Enforced: Subject Pattern Matching Consistency

Strengths:

  • Token-based matching (dot-separated) matches NATS conventions
  • "*" matches exactly one token
  • ">" matches one or more tokens (only at end)
  • Recursive matching is clear and correct

Gaps Identified:

  1. MatchActorPattern has two code paths

    • Simple patterns (no dots): prefix/suffix wildcard matching (lines 183-193)
    • Complex patterns (with dots): token-based matching via MatchNamespacePattern()
    • Implication: Inconsistency between actor ID patterns and namespace patterns
    • Example: "order-" matches "order-123" but "order." matches "order.us.east"
    • Mitigation: Document the difference; clarify when to use dots vs. prefixes
  2. IsWildcardPattern is simple but misses hierarchical wildcards

    • Current: Checks for "*" or ">" anywhere in pattern
    • Fine for EventBus (correct)
    • No issue, but worth documenting for future use

SubscriptionFilter: Composable Filtering

Implementation: pattern.go 85-144 Usage: EventBus.SubscribeWithFilter(), NATSEventBus filtering

Strengths:

  • EventTypes filter is OR (any one matches)
  • ActorPattern filter is AND with EventTypes
  • IsEmpty() correctly detects no filters

No Gaps: Filtering is correctly implemented.


Refactoring Priorities

P1: Add Namespace to Event Metadata (2-3 days)

Why: Required for observability and audit trails

Changes:

type Event struct {
    ID        string
    EventType string
    ActorID   string
    Namespace string  // ← NEW: Logical boundary this event belongs to
    Version   int64
    Data      map[string]interface{}
    Timestamp time.Time
    Metadata  map[string]string
}

Impact:

  • Enables: event.Namespace in replayed events
  • Enables: Filtering on namespace in read models
  • Enables: Clear audit trail of which namespace an event came from
  • Breaking: None (new field, optional in JSON via omitempty)

Testing: Verify namespace is set during SaveEvent() and persists through replay


P2: Add Namespace Validation (1 day)

Why: Prevent silent behavior changes from namespace sanitization

Changes:

// In application or aether package
func ValidateNamespace(ns string) error {
    if ns == "" {
        return errors.New("namespace cannot be empty")
    }
    // Allow alphanumeric, hyphens, underscores, dots for hierarchies
    if !regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).MatchString(ns) {
        return fmt.Errorf("namespace contains invalid characters: %q", ns)
    }
    return nil
}

// Update constructors
func NewJetStreamEventStoreWithNamespace(conn, streamName, namespace string) error {
    if err := ValidateNamespace(namespace); err != nil {
        return err  // Reject immediately, don't sanitize silently
    }
    // ... rest of initialization
}

Impact:

  • Prevents: Silent sanitization of invalid names
  • Enables: Clear error messages for misconfigured namespaces
  • Breaking: Existing invalid namespaces may fail validation (rare, can be migrated)

Testing: TestValidateNamespace cases for valid/invalid formats


P3: Create NamespacedEventBus Wrapper (2-3 days)

Why: Improve API safety; prevent accidental wildcard subscriptions

Changes:

type NamespacedEventBus struct {
    bus       *EventBus
    namespace string  // Bound to single namespace
}

func (neb *NamespacedEventBus) Subscribe() <-chan *Event {
    // ALWAYS exact match, never wildcard
    // Reject if pattern contains "*" or ">"
    return neb.bus.Subscribe(neb.namespace)
}

func (neb *NamespacedEventBus) SubscribeWithFilter(filter *SubscriptionFilter) <-chan *Event {
    return neb.bus.SubscribeWithFilter(neb.namespace, filter)
}

func (neb *NamespacedEventBus) Publish(event *Event) {
    // Verify event is for this namespace (once namespace field is added)
    if event.Namespace != neb.namespace {
        return errors.New("event namespace mismatch")
    }
    neb.bus.Publish(neb.namespace, event)
}

Impact:

  • Prevents: Accidental wildcard subscriptions
  • Enables: Easier-to-use API for single-namespace scenarios
  • Breaking: None (additive wrapper)

Testing: TestNamespacedEventBus verifies wildcards are rejected


P4: Cross-Namespace Integration Tests (1-2 days)

Why: Verify isolation at all layers (storage, local pub/sub, cross-node)

Test Cases:

  1. SaveEvent(store1, ns1) → GetEvents(store2, ns2) returns empty
  2. Publish(ns1) → Subscribe(ns2) receives nothing
  3. SaveEvent(store1, ns1, "prod.orders") → GetEvents(store2, "prod.users") returns empty
  4. Publish("prod.orders") → Subscribe("prod.*") receives it
  5. Publish("prod.orders") → Subscribe(">") receives it
  6. Cross-node: Publish on node1 → Subscribe on node2, verify namespace isolation

Impact:

  • Confidence: Safety invariant is enforced at all layers
  • Coverage: Currently no integration tests for namespace isolation
  • Effort: 1-2 days

P5: Document Namespace Hierarchies and Patterns (1 day)

Why: Clarify intended use of dot-separated namespaces

Documentation:

  • Define hierarchical namespace format: {domain}.{environment}.{tenant}
  • Examples:
    • "orders.prod.acme" - ACME Corp, production orders domain
    • "users.staging.acme" - ACME Corp, staging users domain
    • "acme.>" - All namespaces for ACME tenant
    • ".prod." - All production namespaces across all domains
  • Warning: Dots are sanitized to underscores at JetStream level
    • "orders.prod" → stored in "orders_prod_events" stream
    • Namespace isolation still works, but stream name is sanitized

Impact:

  • Clarity: Applications understand how to structure namespaces
  • Examples: Clear patterns for multi-tenant and multi-domain setups

Security Considerations

Wildcard Subscriptions Bypass Isolation

Intentional Design: Wildcard patterns receive events from multiple namespaces. This is necessary for:

  • Cross-cutting concerns (logging, auditing, monitoring)
  • Trusted system components (ops, admin code)
  • Distributed tracing (observability across boundaries)

Documented In:

  • eventbus.go lines 10-16 (EventBroadcaster)
  • eventbus.go lines 63-66 (EventBus)
  • nats_eventbus.go lines 15-20 (NATSEventBus)
  • pattern.go lines 19-26 (MatchNamespacePattern)

Mitigations:

  1. Code Review: Audit any use of "*" or ">" in subscriptions
  2. Logging: NATSEventBus logs wildcard subscriptions (lines 100-101)
  3. Metrics: Wildcard subscriptions are tracked (RecordSubscribe)
  4. Documentation: Explicit warnings in all relevant locations

Risk Assessment:

  • Low if: Wildcard access is restricted to trusted components (ops, admin code paths)
  • High if: User-facing APIs accept subscription patterns from external clients
  • High if: Feature flags control subscription patterns (default should be exact, not wildcard)

Recommended Controls:

  • Restrict wildcard subscriptions to admin/ops code
  • Audit logs for all wildcard subscription creation
  • Code review checklist: "Does this code use wildcard subscriptions? If yes, is it justified?"
  • Disable wildcard subscriptions by default in application APIs

Testing Strategy

Existing Tests (namespace_test.go, pattern_test.go)

  • ✓ Stream naming with and without namespace
  • ✓ Sanitization of special characters
  • ✓ Pattern matching (single token, multi-token, wildcards)
  • ✓ Actor type extraction

Missing Integration Tests

  • ✗ SaveEvent in one namespace invisible to another namespace's GetEvents
  • ✗ Publish to exact namespace not received by different exact namespace
  • ✗ Publish to "prod.orders" IS received by Subscribe("prod.*")
  • ✗ Cross-node namespace isolation
// store_namespace_isolation_test.go
func TestStorageIsolation_DifferentNamespaces(t *testing.T)
func TestStorageIsolation_SameStreamName(t *testing.T)
func TestStorageIsolation_ActorIDConflict(t *testing.T)

// eventbus_namespace_isolation_test.go
func TestEventBusExactNamespace(t *testing.T)
func TestEventBusWildcardPattern(t *testing.T)
func TestEventBusFilterWithNamespace(t *testing.T)

// nats_eventbus_namespace_isolation_integration_test.go
func TestNATSEventBusCrossNodeIsolation(t *testing.T)
func TestNATSEventBusWildcardCrossNode(t *testing.T)

Implementation Checklist

  • Phase 1: Core (0 risk)

    • Add namespace field to Event struct (omitempty JSON)
    • Update SaveEvent to populate event.Namespace
    • Create integration tests for storage isolation
  • Phase 2: Validation (low risk)

    • Create ValidateNamespace() function
    • Update NewJetStreamEventStoreWithNamespace to validate
    • Document namespace format conventions
  • Phase 3: Convenience (low risk)

    • Create NamespacedEventBus wrapper
    • Add examples for both generic and namespaced use cases
  • Phase 4: Observability (low risk)

    • Document hierarchical namespace patterns
    • Update examples with prod/staging/tenant scenarios
    • Add to vision.md or architecture guide
  • Phase 5: Safety (no code changes)

    • Add wildcard subscription warnings to API docs
    • Create audit logging guide (optional implementation)
    • Add security checklist to CLAUDE.md

References

Full Domain Model: DOMAIN_MODEL_NAMESPACE_ISOLATION.md

Key Code Locations:

  • EventBus: /aether/eventbus.go (268 lines)
  • NATSEventBus: /aether/nats_eventbus.go (231 lines)
  • Pattern Matching: /aether/pattern.go (197 lines)
  • JetStream Storage: /aether/store/jetstream.go (382 lines)
  • Namespace Tests: /aether/store/namespace_test.go (125 lines)

Design Philosophy:

  • Vision: /aether/vision.md ("Primitives Over Frameworks")
  • Organization: /aether/CLAUDE.md ("Namespace Isolation" bounded context)

Conclusion

Status: Implementation Meets Domain Model (Mostly)

The Namespace Isolation context has strong primitive foundations:

  • ✓ Storage-layer isolation (separate JetStream streams)
  • ✓ Pub/sub isolation (exactSubscribers per namespace)
  • ✓ Pattern matching (NATS-native wildcards)
  • ✓ Safety warnings (documented wildcard risks)

Remaining Work:

  1. Add namespace metadata to events (enables audit trails, tracing)
  2. Add explicit validation (prevents silent sanitization)
  3. Improve API ergonomics (NamespacedEventBus wrapper)
  4. Integration tests (confidence in safety invariants)

No architectural changes needed. Refactoring is additive: improvements to observability, safety, and usability without changing core design.

The system correctly enforces the core invariant: Events in namespace X are invisible to queries from namespace Y, except through explicit wildcard subscriptions by trusted components.