# How ai-memory Audit Log Attributes Writes to Specific DB Users vs Root Operators

> Learn how the ai-memory audit log attributes writes to specific DB users versus root operators by examining author_id column entries. Understand your database activity.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-30

---

**The ai-memory audit log distinguishes root operators from database users by storing `NULL` in the `author_id` column for root-level actions and the user's 16-byte `UserId` for authenticated operations.**

The **ai-memory** project implements a comprehensive audit trail that tracks every mutating operation through a centralized `audit_log` table. Understanding how this system attributes writes to either anonymous root operators or specific database users is essential for security auditing and compliance in multi-user deployments. This article examines the implementation details in the Rust source code, including the `audit` helper functions, writer interfaces, and reader resolution mechanisms.

## The Audit Log Schema and `author_id` Design

The foundation of user attribution lies in the **nullable `author_id` column** defined in [`crates/ai-memory-store/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/lib.rs)【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/lib.rs#L216-L224】.

This column serves as a **foreign key to the `users` table** but is explicitly optional, allowing the database to store `NULL` values. The schema design enables a simple boolean distinguisher: `NULL` means root operator, `NOT NULL` means authenticated database user.

## Root Operator Operations: Passing `author_id: None`

When the `ai-memory` CLI or other root-level tools execute commands without an authenticated user context, the audit system records these as anonymous operations.

In [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs)【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/ops.rs#L77-L95】, the `audit` helper function accepts `author_id: Option<UserId>` and inserts it directly into the `audit_log` table:

```rust
// Example: Deleting a page as the root operator (no user)
store.writer.delete_page(
    &workspace_id,
    &project_id,
    &page_path,
    None,                        // <-- author_id = NULL
).await?;                       // audit row will have a NULL author

```

The `None` value propagates through the call chain and results in a database `NULL`. When querying these records, left-joining against `users` yields no match, which calling code conventionally displays as "root" or "system".

## Database User Operations: Passing `Some(user_id)`

Authenticated API requests supply a valid **16-byte `UserId`** that flows through the entire write path.

All mutating functions in [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs)【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/writer.rs#L88-L100】accept `author_id: Option<UserId>` as a parameter:

```rust
// Example: Deleting a page as a specific DB user
let user_id = ai_memory_core::UserId::new(); // obtained from auth layer
store.writer.delete_page(
    &workspace_id,
    &project_id,
    &page_path,
    Some(user_id),               // <-- author_id supplied
).await?;                       // audit row will contain this user's ID

```

The `Some(user_id)` variant ensures precise attribution. The `UserId` type is a fixed-size 16-byte identifier (likely a UUID), stored efficiently without additional serialization overhead.

## Resolving Author Names in Audit Queries

The attribution system completes its cycle in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs)【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/reader.rs#L6700-L6720】, where read operations **left-join the `users` table** to resolve human-readable names:

```rust
// Listing audit events with resolved author names
let events = store.reader.list_audit_events(AuditLogFilter::default()).await?;
for ev in events {
    println!(
        "{} – {} – author: {}",
        ev.at,
        ev.op,
        ev.author_name.unwrap_or_else(|| "root".into())
    );
}

```

The left join preserves audit records with `NULL` `author_id` (root operations) while enriching user-attributed records with usernames or display names. This pattern ensures **backward-compatible queries** that handle both attribution types uniformly.

## Key Implementation Files

| File | Responsibility | Critical Lines |
|------|---------------|--------------|
| [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) | Core `audit` and `audit_with_detail` helper functions | 77-95 |
| [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs) | Writer methods accepting `author_id: Option<UserId>` | 88-100 |
| [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs) | Audit query resolution with user table joins | 6700-6720 |
| [`crates/ai-memory-store/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/lib.rs) | Schema definition ensuring nullable `author_id` | 216-224 |

## Summary

- **Root operator attribution**: Pass `author_id: None` → stored as SQL `NULL` → displayed as "root".
- **Database user attribution**: Pass `author_id: Some(UserId)` → stored as 16-byte foreign key → resolved via left join to username.
- **Schema design**: The nullable `author_id` column in `audit_log` provides a single, efficient discriminator.
- **Implementation path**: Writer functions → `audit` helper in [`ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/ops.rs) → database insert → reader resolution with user join.

## Frequently Asked Questions

### How does ai-memory distinguish between system actions and user actions in the audit log?

The system uses the **nullability of the `author_id` column** as the sole discriminator. Root operators pass `None`, resulting in `NULL` database values. Authenticated users pass `Some(user_id)`, populating the foreign key. This design avoids additional boolean columns and leverages standard SQL join semantics for resolution.

### What happens if a user is deleted after performing audited actions?

The left-join pattern in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs)【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/reader.rs#L6700-L6720】uses a **left outer join**, which preserves audit records even when the referenced user no longer exists. The `author_name` field will be `None` in these cases, similar to root operations, but the original `author_id` remains stored in the audit row for forensic reference.

### Can the audit log track which specific root operator performed an action?

Based on the current implementation, **no**. Root operations are indistinguishable from one another—all map to `NULL` `author_id`. Organizations requiring per-operator attribution for administrative actions must implement authentication layers that supply distinct `UserId` values even for service accounts or elevated operators.

### Is the `UserId` type a standard UUID or a custom format?

The `UserId` type referenced throughout `ai-memory` is a **16-byte identifier**, consistent with UUID v4 storage. The code treats it as an opaque type from `ai_memory_core`, ensuring type safety across the audit boundary without exposing internal byte layout to callers.