Backend

Activity log

An audit trail of who did what, when and from where, recorded automatically for account events and on demand for yours.

pleate_backend.audit records who did what, when and from where. Account events are logged automatically; your own actions take one call. Users see their own history (a security page), staff see everything (a back office table), and entries expire on their own.

An entry

json
{
  "id": "6720d3b1c2d3e4f5a6b7c8d9",
  "actor": "6720c1f4a3b2c1d0e9f8a7b6",
  "actor_email": "ada@plate.dev",
  "action": "billing.plan_changed",
  "target_type": "pleate_billing.subscription",
  "target_id": "6720d2aa…",
  "target_repr": "Pro (monthly)",
  "changes": { "plan": ["free", "pro"] },
  "metadata": { "reason": "upgrade" },
  "ip": "203.0.113.24",
  "user_agent": "Mozilla/5.0 …",
  "request_id": "9f8e7d6c5b4a3f2e",
  "created_at": "2026-10-26T10:30:00Z"
}

Recorded automatically

ActionWhen
auth.login / auth.logoutSign in and out.
auth.login_failedWrong password. Only the attempted email is kept.
auth.signup, auth.email_verifiedAccount created, email confirmed.
auth.password_changed, auth.password_resetPassword changed while signed in, or through a reset link.
auth.email_changed, auth.sessions_revokedEmail changed, devices signed out.
account.deletedThe account was deleted.
mfa.enabled, mfa.disabled, mfa.backup_code_usedWhen the mfa app is installed.
oauth.linked, oauth.unlinkedWhen the oauth app is installed.

Logging your own

python
from pleate_backend.audit import log_activity, register

log_activity(
    "billing.plan_changed",
    target=subscription,
    changes={"plan": ["free", "pro"]},
    reason="upgrade",            # extra keyword arguments go to metadata
)

# Every create, update and delete of a model, with field diffs:
register(Product, exclude=("password",))   # catalog.product.created|updated|deleted

The actor defaults to the signed in user of the current request, and IP, user agent and request id come from it too, thanks to AuditContextMiddleware (installed right after AuthenticationMiddleware). log_activity never raises: auditing must not break the action it records. Sensitive keys (password, secret, token, key_hash...) are masked in changes and metadata.

Endpoints

Mounted at /api/activity.

MethodPathPurpose
GET/activityMy activity, newest first. Cursor list (cursor, page_size). Filters action and action__startswith (e.g. auth.).
GET/activity/allStaff only. Page list, search over actor email, target and action. Filters action, action__startswith, actor, target_type, target_id, created_after, created_before (ISO 8601). Ordering created_at.
GET/activity/all/<id>Staff only. One entry.
A security page
const client = usePleateClient();
const feed = useCursorList(["activity", "auth"], (p) => client.activity.mine({ ...p, action__startswith: "auth." }));

<Timeline>
  {feed.items.map((e) => (
    <TimelineItem key={e.id} title={e.action} time={new Date(e.created_at).toLocaleString()} description={e.ip} />
  ))}
</Timeline>
A staff table
const log = useList<ActivityLog>("/activity/all", { ordering: "-created_at", syncUrl: true });
<DataTable columns={columns} getRowId={(e) => e.id} {...log.tableProps} />

Settings

PLEATE["AUDIT"]:

KeyDefaultMeaning
RETENTION_DAYS365Entries expire after this many days (TTL index).
LOG_READSFalseAlso log successful GET requests as api.read.