@atlashub/smartstack-cli 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.documentation/agents.html +147 -40
  2. package/.documentation/apex.html +1 -1
  3. package/.documentation/business-analyse.html +3 -3
  4. package/.documentation/cli-commands.html +2 -2
  5. package/.documentation/commands.html +14 -14
  6. package/.documentation/efcore.html +14 -14
  7. package/.documentation/gitflow.html +12 -12
  8. package/.documentation/hooks.html +41 -3
  9. package/.documentation/index.html +1 -1
  10. package/.documentation/init.html +2 -2
  11. package/.documentation/installation.html +11 -11
  12. package/.documentation/js/app.js +1 -1
  13. package/.documentation/ralph-loop.html +1 -1
  14. package/.documentation/test-web.html +4 -4
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp-entry.mjs +57595 -4569
  18. package/dist/mcp-entry.mjs.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/skills/business-analyse/_shared.md +55 -14
  21. package/templates/skills/business-analyse/steps/step-03-specify.md +63 -0
  22. package/templates/skills/business-analyse/steps/step-04-validate.md +23 -1
  23. package/templates/skills/business-analyse/steps/step-05-handoff.md +136 -53
  24. package/templates/skills/business-analyse/templates/tpl-handoff.md +99 -23
  25. package/templates/skills/controller/templates.md +82 -0
  26. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -0
  27. package/templates/skills/efcore/steps/migration/step-03-validate.md +19 -0
  28. package/templates/skills/review-code/SKILL.md +4 -2
  29. package/templates/skills/review-code/references/owasp-api-top10.md +243 -0
  30. package/templates/skills/review-code/references/security-checklist.md +86 -1
  31. package/templates/skills/review-code/references/smartstack-conventions.md +166 -0
  32. package/templates/skills/workflow/SKILL.md +27 -0
@@ -0,0 +1,243 @@
1
+ <overview>
2
+ OWASP API Security Top 10 checklist adapted for SmartStack (.NET/ASP.NET Core). This is DIFFERENT from the OWASP Top 10 (web application vulnerabilities) -- this list targets API-specific threats.
3
+
4
+ Use this reference when reviewing API controllers, especially those exposed to external clients or public-facing APIs.
5
+ </overview>
6
+
7
+ <api1_bola>
8
+ ## API1 - Broken Object Level Authorization (BOLA/IDOR)
9
+
10
+ **Risk:** Users access other users' resources by manipulating object IDs in requests.
11
+
12
+ **SmartStack check:**
13
+ - [ ] All queries filter by `TenantId` (EF Core global filters active)
14
+ - [ ] `[RequirePermission]` on every endpoint
15
+ - [ ] No raw `Guid` from URL used directly without ownership verification
16
+
17
+ ```csharp
18
+ // BAD: IDOR vulnerability - any authenticated user can access any order
19
+ [HttpGet("{id}")]
20
+ public async Task<ActionResult<OrderDto>> Get(Guid id)
21
+ {
22
+ var order = await _context.Orders.FindAsync(id);
23
+ return Ok(order);
24
+ }
25
+
26
+ // GOOD: Tenant filter via EF Core global filter + explicit check
27
+ [HttpGet("{id}")]
28
+ [RequirePermission(Permissions.Business.Orders.Read)]
29
+ public async Task<ActionResult<OrderDto>> Get(Guid id)
30
+ {
31
+ var order = await _context.Orders
32
+ .FirstOrDefaultAsync(o => o.Id == id); // Global filter ensures TenantId match
33
+ if (order is null) return NotFound();
34
+ return Ok(order.ToDto());
35
+ }
36
+ ```
37
+
38
+ **Detection pattern:**
39
+ ```bash
40
+ grep -rE "FindAsync\(id\)|Find\(id\)" --include="*.cs" | grep -v "TenantId"
41
+ ```
42
+ </api1_bola>
43
+
44
+ <api2_broken_auth>
45
+ ## API2 - Broken Authentication
46
+
47
+ **Risk:** Weak authentication mechanisms allow attackers to impersonate users.
48
+
49
+ **SmartStack check:**
50
+ - [ ] `[Authorize]` on all controllers (except explicit `[AllowAnonymous]`)
51
+ - [ ] JWT tokens validated with issuer, audience, and expiration
52
+ - [ ] Refresh token rotation implemented
53
+ - [ ] Failed login attempts tracked and rate-limited
54
+
55
+ **Detection pattern:**
56
+ ```bash
57
+ grep -rL "\[Authorize\]" --include="*Controller.cs" | grep -v "AuthController"
58
+ ```
59
+ </api2_broken_auth>
60
+
61
+ <api3_broken_property_auth>
62
+ ## API3 - Broken Object Property Level Authorization
63
+
64
+ **Risk:** Users modify properties they shouldn't have access to (mass assignment).
65
+
66
+ **SmartStack check:**
67
+ - [ ] DTOs separate from entities (no direct entity binding)
68
+ - [ ] `CreateDto` and `UpdateDto` contain only writable fields
69
+ - [ ] Sensitive properties (TenantId, CreatedById, Role) NOT in DTOs
70
+ - [ ] No `[FromBody] Entity` binding (always use DTOs)
71
+
72
+ ```csharp
73
+ // BAD: Mass assignment - user can set their own Role
74
+ [HttpPut("{id}")]
75
+ public async Task<ActionResult> Update(Guid id, [FromBody] User user) { ... }
76
+
77
+ // GOOD: DTO limits writable fields
78
+ [HttpPut("{id}")]
79
+ [RequirePermission(Permissions.Business.Users.Update)]
80
+ public async Task<ActionResult> Update(Guid id, [FromBody] UpdateUserDto dto) { ... }
81
+
82
+ public record UpdateUserDto(string Name, string Email); // No Role, no TenantId
83
+ ```
84
+ </api3_broken_property_auth>
85
+
86
+ <api4_unrestricted_consumption>
87
+ ## API4 - Unrestricted Resource Consumption
88
+
89
+ **Risk:** No rate limiting allows API abuse, DDoS, or resource exhaustion.
90
+
91
+ **SmartStack check:**
92
+ - [ ] Rate limiting middleware configured (`Microsoft.AspNetCore.RateLimiting`)
93
+ - [ ] Pagination enforced on list endpoints (max page size)
94
+ - [ ] File upload size limits set
95
+ - [ ] Query complexity limits (no unbounded `Include()`)
96
+
97
+ ```csharp
98
+ // BAD: No pagination limit - can request entire database
99
+ [HttpGet]
100
+ public async Task<ActionResult<List<OrderDto>>> GetAll([FromQuery] int pageSize = 1000000) { ... }
101
+
102
+ // GOOD: Enforced max page size
103
+ [HttpGet]
104
+ public async Task<ActionResult<PagedResult<OrderDto>>> GetAll(
105
+ [FromQuery] int page = 1,
106
+ [FromQuery] int pageSize = 20)
107
+ {
108
+ pageSize = Math.Min(pageSize, 100); // Hard cap
109
+ ...
110
+ }
111
+ ```
112
+ </api4_unrestricted_consumption>
113
+
114
+ <api5_broken_function_auth>
115
+ ## API5 - Broken Function Level Authorization
116
+
117
+ **Risk:** Regular users access admin-level API functions.
118
+
119
+ **SmartStack check:**
120
+ - [ ] Admin endpoints use `platform.administration.*` permissions
121
+ - [ ] Area-based routing enforced (Admin, Support, Business, User)
122
+ - [ ] System account protection (UserType.System, UserType.LocalAdmin)
123
+ - [ ] No permission bypass via direct URL manipulation
124
+
125
+ ```csharp
126
+ // BAD: Missing permission - any authenticated user can delete
127
+ [HttpDelete("{id}")]
128
+ [Authorize]
129
+ public async Task<ActionResult> Delete(Guid id) { ... }
130
+
131
+ // GOOD: Explicit permission check
132
+ [HttpDelete("{id}")]
133
+ [RequirePermission(Permissions.Platform.Administration.Users.Delete)]
134
+ public async Task<ActionResult> Delete(Guid id) { ... }
135
+ ```
136
+ </api5_broken_function_auth>
137
+
138
+ <api6_business_flow>
139
+ ## API6 - Unrestricted Access to Sensitive Business Flows
140
+
141
+ **Risk:** Automated abuse of business processes (mass account creation, coupon abuse).
142
+
143
+ **SmartStack check:**
144
+ - [ ] Rate limiting on sensitive endpoints (registration, password reset)
145
+ - [ ] CAPTCHA or bot detection on public-facing forms
146
+ - [ ] Business flow monitoring and alerting
147
+ - [ ] Idempotency keys for payment/creation operations
148
+ </api6_business_flow>
149
+
150
+ <api7_ssrf>
151
+ ## API7 - Server-Side Request Forgery (SSRF)
152
+
153
+ **Risk:** Attacker makes the server send requests to internal resources.
154
+
155
+ **SmartStack check:**
156
+ - [ ] No user-supplied URLs passed to `HttpClient` without validation
157
+ - [ ] Webhook URLs validated against allowlist
158
+ - [ ] Internal network ranges blocked (127.0.0.1, 10.x, 192.168.x)
159
+
160
+ ```csharp
161
+ // BAD: SSRF - user controls the URL
162
+ [HttpPost("fetch")]
163
+ public async Task<ActionResult> Fetch([FromBody] string url)
164
+ {
165
+ var response = await _httpClient.GetAsync(url);
166
+ return Ok(await response.Content.ReadAsStringAsync());
167
+ }
168
+
169
+ // GOOD: Validate against allowlist
170
+ [HttpPost("webhook")]
171
+ public async Task<ActionResult> ConfigureWebhook([FromBody] WebhookDto dto)
172
+ {
173
+ if (!_webhookValidator.IsAllowedDomain(dto.Url))
174
+ return BadRequest("URL domain not allowed");
175
+ ...
176
+ }
177
+ ```
178
+ </api7_ssrf>
179
+
180
+ <api8_misconfiguration>
181
+ ## API8 - Security Misconfiguration
182
+
183
+ **Risk:** Default configs, verbose errors, missing security headers expose attack surface.
184
+
185
+ **SmartStack check:**
186
+ - [ ] Security headers configured (see security-checklist.md A02)
187
+ - [ ] Error responses don't expose stack traces in production
188
+ - [ ] CORS restricted to known origins (no `AllowAnyOrigin` in production)
189
+ - [ ] Swagger/OpenAPI disabled in production
190
+ - [ ] Debug mode off in production (`ASPNETCORE_ENVIRONMENT=Production`)
191
+
192
+ **Detection pattern:**
193
+ ```bash
194
+ grep -rE "AllowAnyOrigin|EnableDetailedErrors|DeveloperExceptionPage" --include="*.cs"
195
+ ```
196
+ </api8_misconfiguration>
197
+
198
+ <api9_inventory>
199
+ ## API9 - Improper Inventory Management
200
+
201
+ **Risk:** Old or undocumented API endpoints remain exposed without security.
202
+
203
+ **SmartStack check:**
204
+ - [ ] All controllers have `[NavRoute]` attribute (discoverable)
205
+ - [ ] Deprecated endpoints marked with `[Obsolete]`
206
+ - [ ] `[ProducesResponseType]` on every endpoint (API documentation)
207
+ - [ ] No orphan controllers without matching permissions
208
+ </api9_inventory>
209
+
210
+ <api10_unsafe_consumption>
211
+ ## API10 - Unsafe Consumption of APIs
212
+
213
+ **Risk:** Application blindly trusts data from third-party APIs.
214
+
215
+ **SmartStack check:**
216
+ - [ ] External API responses validated/deserialized with typed DTOs
217
+ - [ ] Timeout and retry policies on `HttpClient` (Polly)
218
+ - [ ] Circuit breaker pattern for unreliable external services
219
+ - [ ] External data sanitized before storage
220
+ </api10_unsafe_consumption>
221
+
222
+ <severity_mapping>
223
+ ## Mapping to SmartStack SEC-xxx Categories
224
+
225
+ | OWASP API | SmartStack Check | Severity |
226
+ |-----------|-----------------|----------|
227
+ | API1 BOLA | SEC-001: Missing tenant filter | blocking |
228
+ | API2 Auth | SEC-002: Missing [Authorize] | blocking |
229
+ | API3 Property Auth | SEC-003: Entity binding (mass assignment) | critical |
230
+ | API4 Resource | SEC-004: No pagination limit | warning |
231
+ | API5 Function Auth | SEC-005: Missing [RequirePermission] | blocking |
232
+ | API6 Business Flow | SEC-006: No rate limit on sensitive ops | warning |
233
+ | API7 SSRF | SEC-007: Unvalidated URL in HttpClient | blocking |
234
+ | API8 Misconfig | SEC-008: CORS/Debug/Headers | critical |
235
+ | API9 Inventory | SEC-009: Undocumented endpoint | info |
236
+ | API10 Unsafe API | SEC-010: Unvalidated external data | warning |
237
+ </severity_mapping>
238
+
239
+ <sources>
240
+ - [OWASP API Security Top 10 (2023)](https://owasp.org/API-Security/editions/2023/en/0x11-t10/)
241
+ - [OWASP API Security Project](https://owasp.org/www-project-api-security/)
242
+ - SmartStack RBAC and Multi-tenant documentation
243
+ </sources>
@@ -17,8 +17,35 @@ Configuration hardening:
17
17
 
18
18
  - [ ] No default credentials
19
19
  - [ ] Debug mode disabled in production
20
- - [ ] Secure headers present (CSP, X-Frame-Options, HSTS)
20
+ - [ ] Secure headers present (see below)
21
21
  - [ ] Error messages don't expose internals
22
+
23
+ **Security headers configuration (ASP.NET Core):**
24
+ ```csharp
25
+ // Program.cs
26
+ app.UseHsts(); // HTTP Strict Transport Security (production only)
27
+
28
+ app.Use(async (context, next) =>
29
+ {
30
+ var headers = context.Response.Headers;
31
+ headers["X-Content-Type-Options"] = "nosniff";
32
+ headers["X-Frame-Options"] = "DENY";
33
+ headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
34
+ headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
35
+ headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
36
+ await next();
37
+ });
38
+ ```
39
+
40
+ **Expected headers in responses:**
41
+ | Header | Value | Purpose |
42
+ |--------|-------|---------|
43
+ | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS |
44
+ | `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing |
45
+ | `X-Frame-Options` | `DENY` | Prevent clickjacking |
46
+ | `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer info |
47
+ | `Permissions-Policy` | `camera=(), microphone=()` | Restrict browser features |
48
+ | `Content-Security-Policy` | `default-src 'self'` | Prevent XSS via inline scripts |
22
49
  </a02_security_misconfiguration>
23
50
 
24
51
  <a04_cryptographic_failures>
@@ -71,6 +98,64 @@ Access control requirements:
71
98
  ✓ No vertical escalation (user → admin functions)
72
99
  </authorization>
73
100
 
101
+ <rate_limiting>
102
+ ## Rate Limiting & Throttling
103
+
104
+ **ASP.NET Core built-in middleware** (`Microsoft.AspNetCore.RateLimiting`):
105
+
106
+ ```csharp
107
+ // Program.cs
108
+ builder.Services.AddRateLimiter(options =>
109
+ {
110
+ // Global fixed window: 100 requests per minute
111
+ options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
112
+ RateLimitPartition.GetFixedWindowLimiter(
113
+ partitionKey: context.User?.FindFirst("tenant_id")?.Value ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
114
+ factory: _ => new FixedWindowRateLimiterOptions
115
+ {
116
+ PermitLimit = 100,
117
+ Window = TimeSpan.FromMinutes(1),
118
+ QueueLimit = 0
119
+ }));
120
+
121
+ // Named policy for sensitive endpoints
122
+ options.AddFixedWindowLimiter("auth", opt =>
123
+ {
124
+ opt.PermitLimit = 5;
125
+ opt.Window = TimeSpan.FromMinutes(15);
126
+ opt.QueueLimit = 0;
127
+ });
128
+
129
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
130
+ });
131
+
132
+ app.UseRateLimiter();
133
+ ```
134
+
135
+ **Controller usage:**
136
+ ```csharp
137
+ [EnableRateLimiting("auth")]
138
+ [HttpPost("login")]
139
+ [AllowAnonymous]
140
+ public async Task<ActionResult> Login([FromBody] LoginDto dto) { ... }
141
+ ```
142
+
143
+ **Available strategies:**
144
+ | Strategy | Use case |
145
+ |----------|----------|
146
+ | Fixed Window | General API protection |
147
+ | Sliding Window | Smoother rate distribution |
148
+ | Token Bucket | Burst-tolerant endpoints |
149
+ | Concurrency | Limit parallel requests (file upload, reports) |
150
+
151
+ **Checklist:**
152
+ - [ ] Global rate limiter configured
153
+ - [ ] Partition by tenant (multi-tenant) or IP (anonymous)
154
+ - [ ] Stricter limits on auth endpoints (login, register, password reset)
155
+ - [ ] `429 Too Many Requests` returned with `Retry-After` header
156
+ - [ ] Rate limit headers present (`X-RateLimit-Limit`, `X-RateLimit-Remaining`)
157
+ </rate_limiting>
158
+
74
159
  <search_patterns>
75
160
  Grep patterns for vulnerability detection:
76
161
 
@@ -105,6 +105,172 @@ public class Permission : SystemEntity
105
105
  </system_entity>
106
106
  </entities>
107
107
 
108
+ <value_objects>
109
+ ## Value Objects (DDD Pattern)
110
+
111
+ Value Objects are immutable types defined by their attributes rather than identity. Use them to replace primitive types that carry domain meaning and validation.
112
+
113
+ **When to use a Value Object:**
114
+ | Instead of | Use | Why |
115
+ |------------|-----|-----|
116
+ | `string Email` | `Email` record | Self-validating, prevents invalid state |
117
+ | `decimal Amount` + `string Currency` | `Money` record | Always consistent pair |
118
+ | `string Street` + `string City` + ... | `Address` record | Cohesive group |
119
+ | `DateTime Start` + `DateTime End` | `DateRange` record | Enforces Start < End invariant |
120
+
121
+ **Structure (C# record):**
122
+ ```csharp
123
+ // Domain/ValueObjects/Email.cs
124
+ public sealed record Email
125
+ {
126
+ public string Value { get; }
127
+
128
+ public Email(string value)
129
+ {
130
+ if (string.IsNullOrWhiteSpace(value))
131
+ throw new DomainException("Email is required");
132
+ if (!value.Contains('@'))
133
+ throw new DomainException("Invalid email format");
134
+
135
+ Value = value.Trim().ToLowerInvariant();
136
+ }
137
+
138
+ public static implicit operator string(Email email) => email.Value;
139
+ public override string ToString() => Value;
140
+ }
141
+ ```
142
+
143
+ **EF Core mapping:**
144
+ ```csharp
145
+ // Option 1: Owned type (separate columns)
146
+ builder.OwnsOne(e => e.Email, email =>
147
+ {
148
+ email.Property(v => v.Value).HasColumnName("Email").HasMaxLength(256);
149
+ });
150
+
151
+ // Option 2: Value conversion (single column)
152
+ builder.Property(e => e.Email)
153
+ .HasConversion(
154
+ v => v.Value,
155
+ v => new Email(v))
156
+ .HasMaxLength(256);
157
+ ```
158
+
159
+ **Entity vs Value Object:**
160
+ | Criteria | Entity | Value Object |
161
+ |----------|--------|-------------|
162
+ | Identity | Has `Id` (Guid) | No identity |
163
+ | Equality | By Id | By all properties |
164
+ | Mutability | Mutable (behavior methods) | Immutable (records) |
165
+ | Lifecycle | Independent | Owned by entity |
166
+ | Example | `User`, `Order` | `Email`, `Money`, `Address` |
167
+
168
+ **Code review detection:** If an entity has `string Email`, `string Phone`, `decimal Amount` + `string Currency` as separate primitives, suggest replacing with Value Objects.
169
+ </value_objects>
170
+
171
+ <domain_events>
172
+ ## Domain Events
173
+
174
+ Domain Events capture something that happened in the domain. They enable decoupled communication between aggregates and complement the existing Workflow trigger system.
175
+
176
+ **Relationship with Workflow Triggers:**
177
+ | Mechanism | Scope | Use case |
178
+ |-----------|-------|----------|
179
+ | Domain Events (MediatR) | In-process, synchronous | Cache invalidation, audit logging, read-model updates |
180
+ | Workflow Triggers (`TriggerAsync`) | Cross-process, async | Emails, webhooks, multi-step business processes |
181
+ | **Combined** | Domain Event handler calls `TriggerAsync()` | Best of both worlds |
182
+
183
+ **Structure:**
184
+ ```csharp
185
+ // Domain/Events/OrderCreatedEvent.cs
186
+ public sealed record OrderCreatedEvent(
187
+ Guid OrderId,
188
+ Guid TenantId,
189
+ Guid CreatedById,
190
+ DateTime OccurredAt) : IDomainEvent;
191
+
192
+ // Domain/Common/IDomainEvent.cs
193
+ public interface IDomainEvent : INotification { } // MediatR marker
194
+ ```
195
+
196
+ **Publishing from entities:**
197
+ ```csharp
198
+ public class Order : BaseEntity, ITenantEntity
199
+ {
200
+ private readonly List<IDomainEvent> _domainEvents = new();
201
+ public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
202
+
203
+ public static Order Create(Guid tenantId, string description, Guid createdById)
204
+ {
205
+ var order = new Order
206
+ {
207
+ Id = Guid.NewGuid(),
208
+ TenantId = tenantId,
209
+ Description = description
210
+ };
211
+
212
+ order._domainEvents.Add(new OrderCreatedEvent(
213
+ order.Id, tenantId, createdById, DateTime.UtcNow));
214
+
215
+ return order;
216
+ }
217
+
218
+ public void ClearDomainEvents() => _domainEvents.Clear();
219
+ }
220
+ ```
221
+
222
+ **Handling events:**
223
+ ```csharp
224
+ // Application/EventHandlers/OrderCreatedEventHandler.cs
225
+ public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
226
+ {
227
+ private readonly IWorkflowService _workflowService;
228
+ private readonly ILogger<OrderCreatedEventHandler> _logger;
229
+
230
+ public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
231
+ {
232
+ _logger.LogInformation("Order {OrderId} created", notification.OrderId);
233
+
234
+ // Bridge to Workflow triggers for cross-process actions
235
+ await _workflowService.TriggerAsync("order.created",
236
+ new Dictionary<string, object>
237
+ {
238
+ ["orderId"] = notification.OrderId,
239
+ ["tenantId"] = notification.TenantId
240
+ }, ct: ct);
241
+ }
242
+ }
243
+ ```
244
+
245
+ **Dispatching (in DbContext):**
246
+ ```csharp
247
+ // Infrastructure/Persistence/ApplicationDbContext.cs
248
+ public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
249
+ {
250
+ var entities = ChangeTracker.Entries<BaseEntity>()
251
+ .Where(e => e.Entity.DomainEvents.Any())
252
+ .ToList();
253
+
254
+ var events = entities.SelectMany(e => e.Entity.DomainEvents).ToList();
255
+
256
+ var result = await base.SaveChangesAsync(ct);
257
+
258
+ foreach (var @event in events)
259
+ await _mediator.Publish(@event, ct);
260
+
261
+ foreach (var entity in entities)
262
+ entity.Entity.ClearDomainEvents();
263
+
264
+ return result;
265
+ }
266
+ ```
267
+
268
+ **When to add Domain Events (code review check):**
269
+ - Entity has side effects in `Create()` or behavior methods → Domain Event candidate
270
+ - Service calls `TriggerAsync()` right after `SaveChangesAsync()` → Should be Domain Event
271
+ - Multiple services react to the same entity change → Domain Event decouples them
272
+ </domain_events>
273
+
108
274
  <tables>
109
275
  ## Table Naming Conventions
110
276
 
@@ -156,6 +156,33 @@ workflowsApi.execute(workflowId, variables)
156
156
  □ Tests: trigger, emails, variables
157
157
  ```
158
158
 
159
+ ## RELATION WITH DOMAIN EVENTS
160
+
161
+ Workflow triggers (`TriggerAsync`) are the **primary mechanism** for cross-process automation (emails, webhooks, multi-step flows).
162
+
163
+ **Domain Events** (MediatR `INotificationHandler<T>`) are a complementary pattern for **in-process** side effects (audit logging, cache invalidation, read-model updates).
164
+
165
+ **Recommended pattern:** Domain Event handler bridges to Workflow triggers:
166
+ ```csharp
167
+ // OrderCreatedEventHandler calls TriggerAsync for cross-process actions
168
+ public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
169
+ {
170
+ await _workflowService.TriggerAsync("order.created",
171
+ new Dictionary<string, object>{ ["orderId"] = notification.OrderId }, ct: ct);
172
+ }
173
+ ```
174
+
175
+ **When to use which:**
176
+ | Scenario | Mechanism |
177
+ |----------|-----------|
178
+ | Send email after entity creation | Workflow Trigger |
179
+ | Invalidate cache after entity update | Domain Event |
180
+ | Log audit trail | Domain Event |
181
+ | Multi-step business process with delays | Workflow Trigger |
182
+ | Notify multiple services of same change | Domain Event → Workflow Trigger |
183
+
184
+ See `review-code/references/smartstack-conventions.md` (Domain Events section) for the full implementation pattern.
185
+
159
186
  ## ABSOLUTE RULES
160
187
 
161
188
  | DO | DON'T |