Implementing Audit Logging with Logto Sentinel Activities

Logto captures every security-relevant event as a sentinel activity in the PostgreSQL sentinel_activities table, enabling immutable audit trails that record the target, action, result, and policy decision for compliance and threat analysis.

Logto is an open-source identity platform that unifies authentication and authorization. Its sentinel activity system serves as the native audit logging mechanism, storing structured records of sign-in attempts, MFA verifications, and policy decisions directly in your database. This architecture ensures that audit data lives alongside your application data, eliminating external dependencies while maintaining strict data integrity.

Sentinel Activity Database Schema

The audit log is physically stored in the sentinel_activities table defined in packages/schemas/tables/sentinel_activities.sql. This table uses a relational schema optimized for high-insert throughput and time-series queries.

Key columns include:

  • tenant_id – Isolates audit data per tenant in multi-tenant deployments.
  • target_type – Categorizes the subject (e.g., User, Email, Phone).
  • target_hash – A hashed identifier for the subject, ensuring PII is not stored in plain text.
  • action – The security event type, mapped to the SentinelActivityAction enum.
  • action_result – The outcome of the action (Success or Failed).
  • decision – The policy enforcement result (Allowed, Blocked, or Challenge).
  • created_at – Timestamp for retention and filtering.

Type Definitions and Enums

The TypeScript definitions in packages/schemas/src/types/sentinel.ts provide type safety for the audit system. These types ensure consistency between the database layer and application logic.

The core enums include:

  • SentinelActivityAction – Defines event types such as SignIn, Register, MfaVerification, and PasswordReset.
  • SentinelActivityResult – Indicates Success or Failed outcomes.
  • SentinelDecision – Records the sentinel's policy verdict as Allowed, Blocked, or Challenge.

The SentinelActivity interface combines these fields with metadata and timestamp information, creating a strongly-typed contract for all audit records.

Recording Audit Events with insertActivity

To write an audit entry, use the insertActivity function from packages/core/src/queries/sentinel-activities.ts. This helper generates a parameterized SQL INSERT statement and executes it against your PostgreSQL pool.

import { createSentinelActivitiesQueries } from '@/queries/sentinel-activities';
import { SentinelActivityAction, SentinelActivityResult, SentinelDecision } from '@/types/sentinel';

const { insertActivity } = createSentinelActivitiesQueries(pool);

await insertActivity({
  tenantId: 'tenant_123',
  targetType: 'Email',
  targetHash: 'sha256:9f86d08...', // Hashed identifier
  action: SentinelActivityAction.SignIn,
  actionResult: SentinelActivityResult.Success,
  decision: SentinelDecision.Allowed,
  // createdAt and other metadata are auto-populated
});

The function returns the inserted record, confirming the audit trail is persisted before the application continues processing.

Implementing a Custom Sentinel Guard

Sentinel guards are the policy enforcement points that consume and produce audit data. The MessageRateGuard in packages/core/src/sentinel/message-rate-guard.ts demonstrates the pattern: it inserts an activity for each verification attempt, then queries historical data to determine if the current request should be blocked.

When implementing a custom guard, inject the query helpers from createSentinelActivitiesQueries:

import { MessageRateGuard } from '@/sentinel/message-rate-guard';
import { createSentinelActivitiesQueries } from '@/queries/sentinel-activities';

const queries = createSentinelActivitiesQueries(pool);

const guard = new MessageRateGuard(
  {
    insertActivity: queries.insertActivity,
    countActivities: queries.countActivities,
  },
  policyConfig // From your sign-in experience settings
);

// In your route handler
const result = await guard.check({ identifier: email, ip: requestIp });
// The guard internally calls insertActivity, creating the audit log

This pattern ensures that every policy decision is logged immutably at the moment it occurs, creating a forensic trail of why a request was allowed or denied.

Querying Audit Logs for the Admin Console

Retrieving audit data follows standard PostgreSQL patterns. The Logto admin console queries the sentinel_activities table via the management API, respecting the audit_logs_retention_days quota defined per tenant.

Example query structure:

// Management API endpoint simplified
app.get('/api/audit-logs', async (req, res) => {
  const { tenantId, page = 1, limit = 20 } = req.query;
  
  const logs = await pool.query(
    `SELECT * FROM sentinel_activities 
     WHERE tenant_id = $1 
     ORDER BY created_at DESC 
     OFFSET $2 LIMIT $3`,
    [tenantId, (page - 1) * limit, limit]
  );
  
  res.json(logs.rows);
});

The frontend renders these records in the Audit Logs tab, using localization keys from packages/phrases/src/locales/en/translation/admin-console/logs.ts for column headers and retention notices.

Summary

  • Unified storage: Logto uses the sentinel_activities table in packages/schemas/tables/sentinel_activities.sql as the single source of truth for security events and audit trails.
  • Type-safe insertion: The insertActivity function in packages/core/src/queries/sentinel-activities.ts provides a strongly-typed interface for writing audit records.
  • Policy integration: Sentinel guards like MessageRateGuard automatically log decisions by calling insertActivity during request processing.
  • Immutable records: The schema stores hashed identifiers and immutable timestamps, ensuring compliance with privacy regulations while maintaining forensic integrity.
  • Retention control: Audit logs respect tenant-specific quotas, with automatic cleanup based on audit_logs_retention_days.

Frequently Asked Questions

What is the retention period for sentinel activity audit logs?

Logto respects the audit_logs_retention_days value defined in your tenant quota configuration. Records older than this threshold are automatically purged from the sentinel_activities table, balancing compliance requirements with database storage efficiency.

How does Logto protect sensitive identifiers in the audit log?

The target_hash column stores a cryptographic hash of the identifier (e.g., email or phone number) rather than the plain text value. This design prevents PII exposure in the audit trail while still allowing correlation of events targeting the same user via the hash value.

Can I extend sentinel activities to track custom application events?

Yes. You can extend the SentinelActivityAction enum in packages/schemas/src/types/sentinel.ts to include custom actions specific to your application logic. After extending the types, implement a custom guard that calls insertActivity with your new action type, following the pattern established in message-rate-guard.ts.

What is the difference between action_result and decision in the audit record?

The action_result field records whether the underlying operation succeeded (e.g., password validation passed), while the decision field records the sentinel policy outcome (e.g., Blocked due to rate limiting). This distinction allows you to audit both technical failures and policy enforcement actions separately.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →