Event log
identity-service records every catalog event and every sign-in in one table, EventLog, whether or not a webhook rule exists for it. Shellui admin shows it under Identity > Log events and on each user profile. Events are kept for the company's retention period, 7 days by default.
What is recorded
- Every webhook catalog event when it fires: account created or deleted, invitations, SCIM provisioning, group changes, SCIM tokens, magic link requests, and SCIM conflicts
- Sign-ins, as two log-only event types:
| Event type | When |
|---|---|
identity.auth.login.succeeded | A user signed in with OAuth, SAML, magic link, or the Django admin login form |
identity.auth.login.failed | A sign-in was refused. The user is linked when it could be resolved |
Sign-in events cannot trigger webhook rules: anonymous traffic can produce failed sign-ins, and forwarding them would let anyone flood your endpoints.
Scheduled job runs are recorded as two staff-only platform events, described in Platform events (staff only).
identity.user.updated is logged when a tracked profile field changes. changed_fields names those fields. A false value such as active: false is omitted from data (empty values are dropped) and is still present on the webhook body.
Row format
| Column | Content |
|---|---|
id | Sequential id |
company | Company the event belongs to. Empty for a sign-in that failed before the company was known, and for platform events |
user | User the event is about, when there is one. Cleared when the user is deleted or removed from the company |
event_type | Catalog event type |
data | Event payload, compacted (see below) |
created_at | When the event happened |
To keep rows small, data is the webhook payload with:
- empty values (
null, empty strings and lists,false) removed user_idremoved, since it is theusercolumn- secret fields removed: an event type can list fields that are never logged, sent to webhooks or stored. No webhook payload contains a sign-in link or token
Sign-in events store provider, failure_reason, is_staff_at_event, ip_hash (salted hash, never the raw IP), user_agent (truncated), client_timezone, client_device_id_hash, client_country and client_city (GeoIP, when configured).
The table has two indexes, (company, created_at) and (user, created_at). They serve every listing, the user timeline, the retention check, and the purge.
Retention
Each company has a data retention in days (Company.data_retention_days, default 7). Only Shellui operators can change it, in Django admin under Companies > Data retention. Company owners see the value in the admin panel.
The purge_expired_data scheduled job deletes events older than the retention, together with finished webhook deliveries, email-service event posts, SCIM provisioning events, and scheduled job runs. It runs every hour inside the container when REDIS_URL is set.
If events older than retention + 1 day are still stored, the job is not running. The admin panel dashboard, the Log events page, and the Django admin company page then show an error. See Stale events warning.
Admin API
Staff and company owners read the log with an access token or a personal access token, plus company_id, like the other admin endpoints.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/events | Event log, newest first, paginated |
GET | /api/v1/events/{id} | One event |
GET | /api/v1/events/types | Event types with label, description and webhook (false for sign-ins). Platform events are not listed |
GET | /api/v1/events/retention | data_retention_days, oldest_event_at, stale_events |
GET /api/v1/events filters:
| Parameter | Meaning |
|---|---|
event_type | One type, or several separated by commas |
user_id | User id |
user | Case-insensitive part of the user email (also matches data.email, for invitations) |
created_after | ISO 8601 datetime, inclusive |
created_before | ISO 8601 datetime, exclusive |
page, page_size | Pagination, page_size up to 100 (default 20) |
scope | company (default) or platform (Django staff only, see Platform events) |
Example row:
{
"id": 1042,
"company_id": 1,
"created_at": "2026-10-02T09:14:03.120Z",
"event_type": "identity.auth.login.failed",
"label": "Sign-in failed",
"user_id": 42,
"user_email": "ada@acme.com",
"data": {
"provider": "github",
"failure_reason": "Company access is disabled.",
"ip_hash": "b5bb9d80…",
"user_agent": "Mozilla/5.0 …"
}
}
Platform events (staff only)
Scheduled jobs are platform maintenance, not company activity, so their runs are logged without a company and only Django staff can read them:
| Event type | When |
|---|---|
identity.scheduled_job.succeeded | A retry_webhooks or purge_expired_data run finished without error |
identity.scheduled_job.failed | A run failed |
data holds run_id, job, trigger (celery or command), duration_ms, counts and host, plus error_key and error_class on failure. The run itself, with the sanitized error message, is at GET /api/v1/scheduled-jobs/runs/{run_id}. See Scheduled jobs monitoring.
These events never reach company owners or other companies:
GET /api/v1/eventslists the company in the token only, so it never returns themGET /api/v1/events?scope=platformandGET /api/v1/events/{id}?scope=platformreturn them to staff. Other callers get403GET /api/v1/events/typesdoes not list them- they cannot trigger webhook rules and are never forwarded to email-service
They expire after the default retention of 7 days, like the runs.
Related
- Scheduled jobs: the purge job
- Webhooks: the event catalog