# Implementing Audit Logging with Logto Sentinel Activities

> Implement audit logging with Logto sentinel activities. Securely record security events in PostgreSQL, creating immutable audit trails for compliance and threat analysis.

- Repository: [Logto/logto](https://github.com/logto-io/logto)
- Tags: how-to-guide
- Published: 2026-07-04

---

**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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/packages/core/src/queries/sentinel-activities.ts). This helper generates a parameterized SQL `INSERT` statement and executes it against your PostgreSQL pool.

```typescript
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`](https://github.com/logto-io/logto/blob/main/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`:

```typescript
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:

```typescript
// 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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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.