# Backend Admin Audit Schema ## Purpose This document defines the `TradeAuditEvent` schema used by the trading backend to log all admin-facing, operator-facing, and safety-critical state changes. Every audit event is written to the structured logger via `logger.info('[AUDIT] {...}')` with a UTC timestamp prepended. Audit events are not yet persisted to Cosmos — they are log-only. Downstream log aggregation (e.g., Azure Monitor, Log Analytics) is the durable store for audit history. --- ## TradeAuditEvent Schema ```typescript interface TradeAuditEvent { event: string; // Required. Identifies the audit event type (see catalogue below). userId?: string; // Auth user ID performing or triggering the action. profileId?: string; // Trading profile ID relevant to the action (if applicable). symbol?: string; // Asset symbol relevant to the action (if applicable). outcome?: 'accepted' // Action was applied. | 'rejected' // Action was blocked by a rule or safety guard. | 'error'; // Action failed due to a runtime error. reason?: string; // Human-readable explanation for the outcome. details?: Record; // Structured metadata specific to the event type. } ``` Log line format (written via `logger.info`): ```json { "ts": "2026-04-07T10:00:00.000Z", "event": "manual_order_created", "userId": "user-abc", "profileId": "profile-xyz", "symbol": "BTC/USDT", "outcome": "accepted", "reason": "within risk limits", "details": { "side": "BUY", "qty": 0.01, "allocatedCapital": 1000 } } ``` --- ## Event Catalogue All events are emitted via `auditTradeEvent()` in `backend/src/services/apiServer.ts`. | Event | Trigger | Key details fields | |---|---|---| | `manual_order_created` | POST `/api/orders/manual` — order submitted | side, qty, symbol, profileId | | `manual_order_rejected` | Manual order blocked by risk/capital guard | reason, guard name | | `manual_order_error` | Manual order failed at execution | error message | | `profile_control_create` | Chat AI creates a new trading profile | profileName, allocatedCapital | | `profile_control_update` | Chat AI updates an existing profile | profileId, updatedFields | | `profile_control_error` | Chat AI profile action fails | error message | | `chat_profile_control` | Generic chat-initiated profile action | action type, profileId | | `trading_paused` | POST `/internal/trading/pause` — admin pauses trading | pausedBy, reason | | `trading_resumed` | POST `/internal/trading/resume` — admin resumes trading | resumedBy | | `backfill_reverted` | POST `/internal/trading/revert-backfill` — admin reverts exit backfill | symbol, profileId | | `reconciliation_audit` | GET `/api/reconciliation/backfill/audit` — admin reads reconciliation audit | — | | `position_closed_manual` | Admin/operator manually closes a position | symbol, profileId, tradeId | | `order_failure` | Order execution failure recorded | side, qty, reason, tradeId | > Not all events use every field. Absent fields are omitted from the log payload. --- ## Audit Middleware The `auditTradeEvent()` private method in `ApiServer` writes directly to the logger: ```typescript private auditTradeEvent(evt: TradeAuditEvent): void { const payload = { ts: new Date().toISOString(), ...evt }; logger.info(`[AUDIT] ${JSON.stringify(payload)}`); } ``` All audit calls are synchronous and never throw — audit failure must not block the primary operation. --- ## Admin-Scoped Endpoints Routes that require `requireAdmin` middleware are the primary sources of audit events: - `POST /internal/trading/pause` - `POST /internal/trading/resume` - `POST /internal/trading/revert-backfill` - `GET /api/reconciliation/backfill/audit` - `POST /api/admin/dynamic-config` All admin actions must produce an audit event with `userId` set from `authUserId` on the authenticated request. --- ## Persistence Audit events are written to two sinks simultaneously: 1. **Structured log** — always, via `logger.info('[AUDIT] ...')`. Zero runtime dependency. 2. **Cosmos `audit-events` container** — best-effort, via `persistAuditEvent()` in `auditRepository.ts`. Silently skipped if Cosmos is not configured or the write fails. ### Activating Cosmos Persistence Create the container in your Cosmos database: | Setting | Value | |---|---| | Container name | `audit-events` | | Partition key | `/productId` | | TTL (Time To Live) | `7776000` seconds (90 days) | | Throughput | Shared or dedicated — start with 400 RU/s | Once the container exists and `COSMOS_ENDPOINT` / `COSMOS_KEY` are configured, all `auditTradeEvent()` calls will persist records automatically. ## Admin Audit Endpoint `GET /api/admin/audit` (admin-only) queries the `audit-events` container: ``` GET /api/admin/audit?userId=user-123&event=manual_order_created&since=1712500000000&limit=50 ``` Query parameters (all optional): | Parameter | Type | Description | |---|---|---| | `userId` | string | Filter by user ID | | `event` | string | Filter by event name (exact match) | | `since` | number | Unix epoch ms — return events newer than this timestamp | | `limit` | number | Max records to return (default 100, max 500) | Response: `{ records: AuditEventDocument[], count: number }`