Audit logs
Streams
| Stream | Storage | Retention | Access |
|---|---|---|---|
PostHog api_request events | PostHog cloud | Per project config | Workspace members |
| Stripe events table | Postgres stripe_events | Permanent | DB readers |
| CloudWatch / stdout logs | CloudWatch (or stdout) | 30 days (default) | AWS IAM principals |
What's captured
AuditLogMiddleware enqueues a row per request with:
requestIduserId,userRole,orgId(if authenticated)method,path,statusCoderesult(success / failure),errorCodeduration_msuserAgent,ip- timestamp
No bodies, no query params, no headers beyond User-Agent and X-Forwarded-For.
What's NOT captured
- Request body — could contain passwords, OAuth codes, etc.
- Response body — could contain PII.
- Query string — could contain
?token=. - Authorization header — obviously.
If you need to add a sensitive field deliberately, gate it behind an env flag and redact in serialization.
Sensitive event sources
Some operations should produce richer audit rows than the default middleware:
| Operation | Recommended audit pattern |
|---|---|
| Admin set-role | log targetUserId, oldRole, newRole |
| Admin ban-user | log targetUserId, reason |
| Agent status change | log agentId, oldStatus, newStatus |
| Subscription cancellation | log subscriptionId, tierName |
| Tier quota change | log tierId, metricSlug, oldQuota, newQuota |
Use AuditLoggerService.log({ event, properties }) from services rather than relying on the HTTP-level middleware.
Access control
- PostHog: gate via PostHog's own RBAC. Limit access to security + ops.
- DB: anyone with
READon the marketplace DB can readstripe_events. Consider a read-only role and rotate which IAM principals can assume it. - CloudWatch: IAM-controlled.
Retention
| Stream | Default | Recommended |
|---|---|---|
| PostHog events | per project | 1 year for compliance, longer for forensics if needed |
stripe_events | forever | OK as-is |
audit_logs table | unused | If you start writing here, 1-year retention with archival to S3 Glacier |
| CloudWatch | 30 days | Bump to 1 year if you don't have a separate forwarder |
Investigation playbook
| Question | Where to look |
|---|---|
| "Who deleted this agent?" | PostHog path = '/agents/<id>' AND method = 'DELETE' |
| "Why did this user's checkout fail?" | PostHog userId = <id> AND path ~ 'checkout'; then cross to stripe_events |
| "What endpoints did this admin call yesterday?" | PostHog userId = <admin> time range |
| "Was there a 5xx spike?" | PostHog result = failure AND statusCode >= 500 time chart |
Adding alerts
In PostHog, set up:
- 5xx error rate alert.
errorCode = 'QUOTA_EXCEEDED'rate alert (signals tier limits too tight).- Specific path latency alert (e.g.
/me/agents/.*/consume> 200ms).
Pipe alerts to PagerDuty / Slack / your tool of choice.
Compliance angle
The audit log:
- Demonstrates user activity for SOC 2 access review.
- Supports incident forensics.
- Provides evidence for billing disputes (combined with
stripe_events).
It does NOT:
- Cover agent-side runtime activity (that's the agent's own audit log).
- Cover infrastructure-level events (CloudTrail does that).