Audit logs
Every security-relevant action emits an audit entry into audit_logs.
Shape
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
organization_id UUID NOT NULL,
actor_id UUID, -- nullable: anonymous events
actor_type VARCHAR(20), -- 'user', 'api_key', 'system'
action VARCHAR(100),
resource_type VARCHAR(50),
resource_id TEXT, -- string form, not necessarily UUID
status VARCHAR(20), -- 'success', 'failure'
user_agent TEXT,
ip_address TEXT,
metadata JSONB,
created_at TIMESTAMPTZ
);
::: tip No foreign keys Audit logs deliberately have NO foreign keys to users or organizations. Compliance reviews require audit trails to survive deletion of their subjects — orphaning a row is intended behavior. :::
Actions emitted by the SDK
| Action | When |
|---|---|
auth.register | A new org + owner user was created. |
auth.login | Successful password login. |
auth.login.failed | Failed login (unknown email or wrong password). |
auth.login.mfa_required | Login returned mfa_required. |
auth.logout | Authenticated logout. |
auth.mfa.enabled | MFA enrollment confirmed. |
auth.mfa.disabled | MFA disabled (password-gated). |
auth.mfa.step_up | Step-up verification succeeded. |
auth.mfa.challenge.succeeded | MFA login challenge passed. |
auth.mfa.challenge.failed | MFA login challenge failed. |
auth.mfa.challenge.locked | Brute-force lockout triggered. |
auth.mfa.recovery_codes_regenerated | User minted new recovery codes. |
auth.passkey.added | Passkey enrolled. |
auth.passkey.removed | Passkey deleted. |
auth.passkey.login | Passkey sign-in. |
auth.email_otp.login | Email-OTP sign-in. |
auth.email_otp.mfa_required | Email-OTP verify returned mfa_required. |
auth.oauth.google.linked / auth.oauth.github.linked | OAuth sign-in. |
auth.password_reset.consumed | Password reset succeeded. |
auth.trusted_browser.added / auth.trusted_browser.revoked | Trusted-browser lifecycle. |
users.email_verified | Verification token consumed. |
users.password_changed | User-initiated password change. |
members.invited / members.invitation_revoked / members.invitation_resent / members.invitation_accepted | Invitation lifecycle. |
members.role_changed / members.removed | Membership mutations. |
Sink
Entries go through AuditService::log, which is a fire-and-forget async
write. Failures are logged but never affect the originating request.
For production deployments that need higher durability (every entry must
land), wrap or replace AuditService to write through a guaranteed sink
(SQS, Kafka, etc.).
Reading entries
The SDK doesn't ship a query API for audit logs. They live in your
Postgres — SELECT them directly:
SELECT created_at, action, status, actor_id, resource_id, metadata
FROM IdentSphere.audit_logs
WHERE organization_id = '...'
ORDER BY created_at DESC
LIMIT 100;
The optional identsphere-audit-export premium crate (not in v0.1 OSS) adds a
typed query interface, CSV/JSONL export, and S3 archival.
What's NOT audited
The SDK does NOT audit read endpoints (GET /v1/users/me, GET /v1/auth/session,
etc.). Audit logs are intended for authentication AND authorization
mutations — record everything that changes security state.
If you need read-audit (BAA / HIPAA), add it at the middleware level.
Retention
The SDK does not implement retention policies. Add a scheduled job
(pg_cron, cron + psql, etc.) that deletes entries older than your
compliance window:
DELETE FROM IdentSphere.audit_logs
WHERE created_at < now() - interval '7 years';