Squash and merge of version-0.1 Co-authored-by: James Wampler <james@wamp.dev> Co-committed-by: James Wampler <james@wamp.dev>
7.2 KiB
Executable File
Step 7: Audit Logging & Webhooks
Goal
Implement the audit trail system (recording all changes to flags, environments, segments, etc.) and the webhook system (notifying external systems of flag state changes).
Audit Logging
What Gets Audited
Every create, update, and delete operation on:
- Features and FeatureStates
- Environments
- Segments
- Identities (admin operations)
- API Keys (create/revoke)
- User permissions
- Organization settings
- Project settings
AuditLog Domain Model
// MicCheck.Api/Audit/AuditLog.cs (already defined in Step 2)
// Key fields:
// ResourceType: "Feature", "FeatureState", "Environment", etc.
// ResourceId: string representation of the entity's PK
// Action: "Created", "Updated", "Deleted"
// Changes: JSON diff of before/after values (for Updates)
// ActorUserId: null for API key operations without user context
Audit Service
Lives in MicCheck.Api/Audit/. Injects MicCheckDbContext directly.
// MicCheck.Api/Audit/AuditService.cs
public class AuditService
{
private readonly MicCheckDbContext _db;
public Task RecordAsync(
string resourceType,
string resourceId,
string action,
int organizationId,
int? projectId = null,
int? environmentId = null,
int? actorUserId = null,
object? before = null,
object? after = null,
CancellationToken ct = default);
}
Change diffing:
- Serialize
beforeandafterobjects to JSON - Store both in
Changesas{ "before": {...}, "after": {...} } - Use
System.Text.Jsonfor serialization
Integration with Services
Audit logging is called directly from other service methods:
// In FeatureService.CreateAsync:
await _auditService.RecordAsync(
resourceType: "Feature",
resourceId: feature.Id.ToString(),
action: "Created",
organizationId: project.OrganizationId,
projectId: feature.ProjectId,
after: feature);
Audit Log Queries
AuditService exposes a query method returning domain objects; the controller maps to response DTOs.
Support filtering by:
- Date range (
from,to) - Resource type
- Action type
- Project
- Environment
- Actor user
Always ordered by CreatedAt DESC. Paginated (default 20, max 100).
// MicCheck.Api/Audit/AuditController.cs
[HttpGet]
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> List(
int id, [FromQuery] AuditLogFilter filter, CancellationToken ct)
{
var (logs, total) = await _auditService.ListAsync(id, filter, ct);
return Ok(new PaginatedResponse<AuditLogResponse>(
total, null, null,
logs.Select(AuditLogResponse.From).ToList()));
}
Webhooks
Webhook Triggers
Webhooks fire on:
FLAG_UPDATED— anyFeatureStatechangeFLAG_DELETED— feature deletedAUDIT_LOG_CREATED— any audit log entry (organization webhooks)
Webhook Payload
{
"data": {
"changed_by": "user@example.com",
"timestamp": "2026-04-06T12:00:00Z",
"new_state": {
"id": 42,
"enabled": true,
"feature_state_value": null,
"feature": { "id": 1, "name": "dark_mode" },
"environment": { "id": 5, "name": "Production" }
},
"previous_state": { ... }
},
"event_type": "FLAG_UPDATED"
}
Webhook Event & Dispatcher
Live in MicCheck.Api/Webhooks/. Dispatcher injects MicCheckDbContext directly.
// MicCheck.Api/Webhooks/WebhookEvent.cs
public class WebhookEvent
{
public required string EventType { get; init; }
public required object Data { get; init; }
public int? EnvironmentId { get; init; }
public int OrganizationId { get; init; }
}
// MicCheck.Api/Webhooks/WebhookDispatcher.cs
public class WebhookDispatcher
{
private readonly MicCheckDbContext _db;
private readonly IHttpClientFactory _httpClientFactory;
// Finds all active webhooks for the environment (and org-level)
// Serializes payload to JSON
// Signs payload with HMAC-SHA256 using webhook secret
// POSTs to webhook URL with timeout of 10 seconds
// Stores delivery result in WebhookDeliveryLog
public Task DispatchAsync(WebhookEvent webhookEvent, CancellationToken ct = default);
}
Webhook Signing
// HMAC-SHA256 signature in header:
// X-Flagsmith-Signature: sha256=<hex_digest>
private static string ComputeSignature(string secret, string payload)
{
var keyBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var hashBytes = HMACSHA256.HashData(keyBytes, payloadBytes);
return "sha256=" + Convert.ToHexString(hashBytes).ToLowerInvariant();
}
Webhook Queue (Async Dispatch)
Lives in MicCheck.Api/Webhooks/. Uses System.Threading.Channels so dispatch does not block the API response.
// MicCheck.Api/Webhooks/WebhookQueue.cs
public class WebhookQueue
{
private readonly Channel<WebhookEvent> _channel = Channel.CreateUnbounded<WebhookEvent>();
public ValueTask EnqueueAsync(WebhookEvent webhookEvent) =>
_channel.Writer.WriteAsync(webhookEvent);
public IAsyncEnumerable<WebhookEvent> ReadAllAsync(CancellationToken ct) =>
_channel.Reader.ReadAllAsync(ct);
}
Retry Background Service
Lives in MicCheck.Infrastructure/Webhooks/. Polls WebhookDeliveryLog and retries failures.
// MicCheck.Infrastructure/Webhooks/WebhookRetryBackgroundService.cs
public class WebhookRetryBackgroundService : BackgroundService
{
// Polls WebhookDeliveryLog for failed deliveries within retry window
// Up to 3 attempts: immediate, +5 min, +30 min
// Reattempts via WebhookDispatcher
}
Retry Strategy
- Up to 3 delivery attempts (immediate, +5 min, +30 min)
- Failed deliveries are logged but do not block the API response
Webhook Admin Endpoints
Controllers map domain objects to/from DTOs in the action method, calling WebhookService for all data access.
GET /api/v1/environments/{apiKey}/webhooks/
POST /api/v1/environments/{apiKey}/webhooks/
PUT /api/v1/environments/{apiKey}/webhooks/{id}/
DELETE /api/v1/environments/{apiKey}/webhooks/{id}/
GET /api/v1/environments/{apiKey}/webhooks/{id}/deliveries/
GET /api/v1/organisations/{id}/webhooks/
POST /api/v1/organisations/{id}/webhooks/
PUT /api/v1/organisations/{id}/webhooks/{id}/
DELETE /api/v1/organisations/{id}/webhooks/{id}/
Acceptance Criteria
- Every create/update/delete in the Admin API produces an
AuditLogrecord - Audit logs correctly capture before/after state for updates
GET /api/v1/organisations/{id}/audit-logs/returns paginated, filtered results- Audit controllers map
AuditLogdomain objects toAuditLogResponseDTOs in the action - Webhooks fire within 1 second of a flag state change
- HMAC-SHA256 signature is correct and verifiable by receivers
- Failed webhook deliveries are logged with status code and response body
- Retry logic attempts delivery 3 times with backoff
- Webhook delivery does not block the API response (fire-and-forget via
WebhookQueue) - Unit tests cover: signature computation, payload serialization, retry scheduling
- Unit tests cover: audit diff generation for create/update/delete scenarios