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>
10 KiB
10 KiB
Namespace Isolation: Quick Reference Card
Core Invariant
Events in namespace X must be invisible to queries from namespace Y (except via explicit wildcard subscriptions by trusted components)
Three Enforcement Layers
Layer 1: Memory (EventBus)
Publish("tenant-a", event)
→ exactSubscribers["tenant-a"] ← event ✓
→ exactSubscribers["tenant-b"] (blocked) ✗
→ wildcardSubscribers (">") ← event ✓ (intentional)
Layer 2: Storage (JetStreamEventStore)
NewJetStreamEventStoreWithNamespace(conn, "events", "tenant-a")
→ Stream: "tenant_a_events" (separate NATS stream)
GetEvents(store1) → queries "tenant_a_events" only ✓
GetEvents(store2) → queries "tenant_b_events" only (no cross-namespace) ✓
Layer 3: Network (NATSEventBus)
Publish("tenant-a", event)
→ NATS subject: "aether.events.tenant-a"
Subscribe("tenant-b")
→ NATS subject: "aether.events.tenant-b" (blocked at NATS) ✗
Subscribe("*")
→ NATS subject: "aether.events.*" (intentional wildcard) ✓
Value Objects
| Object | Type | Example | Purpose |
|---|---|---|---|
| Namespace | string | "tenant-a", "prod.orders" | Logical boundary identifier |
| SubjectPattern | string | "", "prod.", "prod.>" | NATS pattern for matching namespaces |
| SubscriptionFilter | struct | {EventTypes: ["OrderPlaced"]} |
Optional filtering (AND logic) |
Commands at a Glance
| Command | Input | Output | Enforces |
|---|---|---|---|
| DefineNamespace | string | Namespace | Namespace Name Safety |
| PublishToNamespace | namespace, event | delivery to subscribers | Namespace Boundary Isolation |
| SubscribeToNamespace | pattern, filter | channel | Isolation (exact) or Bypass (wildcard) |
| CreateNamespacedEventStore | streamName, namespace | EventStore | Namespace Boundary Isolation |
Policies (If X Then Y)
| Trigger | Action | Invariant |
|---|---|---|
| Publish to namespace | Deliver to exact subscribers of that namespace | Isolation enforced |
| Wildcard subscription | Deliver from all matching namespaces | Isolation bypassed (intentional) |
| Save event | Store in namespace-scoped stream | Storage isolation |
| Namespace format invalid | Sanitize (spaces, dots, *, >) to underscores | Name safety |
Code Locations (Key Files)
/aether/
├─ eventbus.go (268 lines)
│ ├─ exactSubscribers[ns] (isolation boundary)
│ ├─ wildcardSubscribers (intentional bypass)
│ └─ Publish() routing logic
│
├─ nats_eventbus.go (231 lines)
│ ├─ NATS subject: aether.events.{namespace}
│ └─ Cross-node replication with pattern support
│
├─ pattern.go (197 lines)
│ ├─ MatchNamespacePattern() (NATS-native matching)
│ ├─ SubscriptionFilter (EventTypes + ActorPattern)
│ └─ IsWildcardPattern() detector
│
└─ store/jetstream.go (382 lines)
├─ Namespace → stream name mapping
├─ sanitizeSubject() (security)
└─ Per-namespace streams
Pattern Matching Rules
Namespace: "prod.orders.acme" (3 tokens)
Pattern → Match?
────────────────
"prod.orders.acme" → ✓ (exact)
"prod.orders.*" → ✓ (* matches "acme")
"prod.>" → ✓ (> matches "orders.acme")
"prod.*" → ✗ (* matches single token only)
"*" → ✗ (* doesn't match dots)
">" → ✓ (> matches everything)
Rules:
• "." separates tokens
• "*" = exactly one token (no dots)
• ">" = one or more tokens (end only)
• Exact string = exact match
Common Operations
Exact Subscription (Isolation Enforced)
bus := aether.NewEventBus()
ch := bus.Subscribe("tenant-abc") // Only tenant-abc events
event := Event{ActorID: "order-123", ...}
bus.Publish("tenant-abc", event) // ch receives ✓
bus.Publish("tenant-def", event) // ch receives nothing ✗
Wildcard Subscription (Isolation Bypassed)
bus := aether.NewEventBus()
ch := bus.Subscribe(">") // ALL namespaces
bus.Publish("tenant-abc", event) // ch receives ✓
bus.Publish("tenant-def", event) // ch receives ✓
// CAUTION: Only for trusted logging/auditing code
Namespaced Event Store
store1 := store.NewJetStreamEventStoreWithNamespace(
natsConn, "events", "tenant-a")
store2 := store.NewJetStreamEventStoreWithNamespace(
natsConn, "events", "tenant-b")
store1.SaveEvent(event) // Goes to "tenant_a_events" stream
store2.GetEvents("order-123", 0) // Queries "tenant_b_events" only
// event not found (different stream)
Filtered Subscription
filter := &aether.SubscriptionFilter{
EventTypes: []string{"OrderPlaced", "OrderShipped"}, // OR logic
ActorPattern: "order-*", // AND with types
}
ch := bus.SubscribeWithFilter("tenant-a", filter)
// Receives only OrderPlaced or OrderShipped events for order-* actors
Sanitization Examples
| Input | Purpose | Output | Example |
|---|---|---|---|
| "prod abc" | Namespace | "prod_abc" | Spaces → underscores |
| "prod.orders" | Namespace | "prod_orders" | Dots → underscores |
| "tenant*abc" | Namespace | "tenant_abc" | Stars → underscores |
| "tenant>abc" | Namespace | "tenant_abc" | Greater → underscores |
Why? NATS subject tokens can't contain these characters.
Invariants: Verification Checklist
- Events in namespace X are NOT visible to namespace Y (exact subscriptions)
- GetEvents(store_a) does NOT return events from store_b
- NATS subjects are correctly namespaced (aether.events.{namespace})
- Pattern matching works correctly (* = one token, > = multiple)
- Wildcard subscriptions are documented and audited
- Namespace names are sanitized before storage
- Cross-node publishing respects namespace boundaries
Anti-Patterns (What NOT to Do)
| ❌ Don't | ✓ Do Instead | Why |
|---|---|---|
Subscribe(getTenantPattern()) |
Validate pattern is exact (or audited) | Wildcards can leak data |
| Accept namespace from untrusted input | Validate/sanitize first | Prevent injection |
| Use namespace as tenant ID directly | Layer namespace over tenant abstraction | Aether is primitives, not framework |
| Rely on wildcard for "secure" observation | Document and audit wildcard use | Wildcards bypass isolation |
| Ignore pattern matching rules | Use NATS-native patterns | Inconsistency breaks isolation |
Security Checklist
For Namespace Isolation Review
- Wildcard subscriptions used only in trusted code (logging, ops, admin)
- Application validates namespace format before use
- No way for external clients to trigger wildcard subscriptions
- Audit logging enabled for all wildcard subscriptions
- Integration tests verify cross-namespace isolation
- Code reviews check for accidental "*" or ">" patterns
- Documentation warns about wildcard risks
For Multi-Tenant Deployments
- Each tenant has distinct namespace (e.g., "tenant-123")
- No global (">" ) subscriptions except trusted ops code
- Namespace validation prevents tenant ID escaping
- Storage streams are completely separate (not shared)
- Cross-tenant queries fail (GetEvents blocks at stream level)
Refactoring Priorities (Quick Decision)
Do Next?
-
P1: Add namespace to Event metadata (2-3 days)
- Impact: HIGH (enables better auditing)
- Risk: LOW (backward compatible)
-
P2: Explicit namespace validation (1 day)
- Impact: MEDIUM (prevents silent errors)
- Risk: LOW (validates format)
-
P3: NamespacedEventBus wrapper (2-3 days)
- Impact: MEDIUM (easier to use safely)
- Risk: LOW (additive)
Skip? P4-P5 are important but lower priority.
Test Cases to Implement
Must-Have Integration Tests
// Storage isolation
SaveEvent(store1, "tenant-a", event)
GetEvents(store2, "tenant-b", event.ActorID) → empty ✓
// Pub/sub isolation
Publish("tenant-a", event)
Subscribe("tenant-a") → receives ✓
Subscribe("tenant-b") → blocked ✗
// Pattern matching
Publish("prod.orders", event)
Subscribe("prod.*") → receives ✓
Subscribe("*.orders") → blocked ✗
// Cross-node
node1.Publish("tenant-a", event)
node2.Subscribe("tenant-b") → blocked ✗
Implementation Status Summary
| Aspect | Status | Notes |
|---|---|---|
| Exact namespace isolation | ✓ Done | EventBus + JetStream enforce it |
| NATS-native patterns | ✓ Done | Subject-level routing |
| Wildcard subscriptions | ✓ Done | Documented as intentional exception |
| Pattern matching consistency | ✓ Done | MatchNamespacePattern() correct |
| Namespace in Event metadata | ✗ Pending | Add for audit trail |
| Explicit validation | ✗ Pending | Add to prevent silent sanitization |
| NamespacedEventBus wrapper | ✗ Pending | Convenience layer |
| Integration tests | ✗ Pending | Verify isolation at all layers |
Glossary
- Namespace: Logical boundary (tenant, domain, environment)
- Pattern: NATS-style wildcard for matching multiple namespaces
- Exact Subscription: Subscribe to specific namespace (isolation enforced)
- Wildcard Subscription: Subscribe with pattern (isolation bypassed, trusted components only)
- Invariant: Business rule that must never be broken
- Subject: NATS address for routing (aether.events.{namespace})
- Stream: JetStream storage container (one per namespace)
- Sanitization: Replace unsafe characters (spaces, dots, *, >) with underscores
Decision Framework
When using namespaces, ask:
-
Is this namespace logically distinct?
- Yes → use separate namespace
- No → same namespace is fine
-
Can users accidentally subscribe to wildcard?
- Yes → add validation
- No → proceed
-
Need cross-namespace visibility?
- Yes → use wildcard (document and audit)
- No → exact subscriptions (isolation enforced)
-
Is this in trusted code?
- Yes → wildcard subscriptions okay (with audit)
- No → exact subscriptions only
More Information
- Full Domain Model: DOMAIN_MODEL_NAMESPACE_ISOLATION.md
- Implementation Gaps: NAMESPACE_ISOLATION_SUMMARY.md
- Architecture Diagrams: NAMESPACE_ISOLATION_ARCHITECTURE.md
- All Contexts: DOMAIN_MODEL_INDEX.md
Last Updated: 2026-01-12 | Status: Complete | Ready for Implementation: Yes