Querying and analytics
Compass keeps every traced authentication flow in PostgreSQL for 30 days and serves it through four read-only endpoints. Use them to debug a failed sign-in, watch authentication latency, and see how a realm’s traffic changes day by day.
All four need view_events, manage_events or manage_realm. Paths are written without a root path, the server default. If SERVER_ROOT_PATH is set, every path gains that prefix.
| Endpoint | Returns |
|---|---|
GET /realms/{realm_name}/compass/v1/stats | Counts and average duration for the whole realm |
GET /realms/{realm_name}/compass/v1/flows | One page of flows with their steps |
GET /realms/{realm_name}/compass/v1/flows/{flow_id} | One flow with its steps |
GET /realms/{realm_name}/compass/v1/activity/daily | Per-day counts for a date window |
Flow statistics
The stats endpoint returns aggregate figures for the realm at a glance:
{
"data": {
"total": 15234,
"success_count": 14102,
"failure_count": 987,
"pending_count": 145,
"avg_duration_ms": 142.5
}
}
| Field | Description |
|---|---|
total | Every recorded flow, including the expired ones |
success_count | Flows with status success |
failure_count | Flows with status failure |
pending_count | Flows still pending. Abandoned flows leave this count after 12 hours, when they become expired |
avg_duration_ms | Average duration_ms over the flows that have one. Null when none does |
There is no expired count, so total can exceed the sum of the other three. Remember that duration_ms is the duration of a flow’s closing step, not the time the user spent signing in. A rising failure_count or avg_duration_ms still tells you something changed in the pipeline.
Listing flows
GET /realms/{realm_name}/compass/v1/flows
The listing follows the shared contract described in Paginated listings: page, limit, order_by, order, filters, and a {data, metadata} response. Each flow comes with its steps, sorted by started_at.
| Parameter | Type | Description |
|---|---|---|
page | integer, 1 or more | Page number. Defaults to 1 |
limit | integer, 1 to 100 | Page size. Defaults to 20 |
order_by | status, started_at, duration_ms or created_at | Sort column. Defaults to created_at |
order | asc or desc | Sort direction. Defaults to desc |
client_id | string | The OAuth client_id of the client, exact match |
user_id | UUID | Flows closed for this user, exact match |
grant_type | string | Exact match, for example authorization_code or password |
status | pending, success, failure or expired | Exact match |
ip_address | string | Case-insensitive match anywhere in the address. Flows without an address never match |
search | string | Same match as ip_address |
identified | boolean | true keeps flows tied to a user, false keeps the anonymous ones |
completed | boolean | true keeps flows that have a completed_at, false the unfinished ones |
from | RFC 3339 date-time | Flows that started at or after this instant, on started_at |
to | RFC 3339 date-time | Flows that started at or before this instant, on started_at |
Filters combine with AND, and a filter sent with an empty value is ignored. Sorting on duration_ms puts unfinished flows last in ascending order and first in descending order.
Any other parameter is refused with 400 invalid_query naming it. That includes offset, which this endpoint does not have: use page. It also includes from_timestamp, to_timestamp, from_date and to_date: the window parameters of this endpoint are from and to.
A flow gets its user_id when it closes. A wrong password or code on the login page leaves the flow pending with no user, so these attempts do not appear under user_id and are not counted as failure. Find them with status=pending and look for a failed step, as in the queries below.
Example queries
Flows that started in a window and closed with a failure:
curl -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "status=failure" \
--data-urlencode "from=2026-10-30T00:00:00Z" \
--data-urlencode "limit=50" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Sign-ins that never closed, oldest first, to see where they stopped:
curl -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "status=pending" \
--data-urlencode "completed=false" \
--data-urlencode "order_by=started_at" \
--data-urlencode "order=asc" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Password grant flows of one client:
curl -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "client_id=my-frontend" \
--data-urlencode "grant_type=password" \
--data-urlencode "limit=10" \
-H "Authorization: Bearer $ADMIN_TOKEN"
The slowest flows first:
curl -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "order_by=duration_ms" \
--data-urlencode "order=desc" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Flow detail
Fetch one flow by id to see all its steps:
{
"data": {
"id": "01914b3c-7e8a-7f5a-b456-789012345678",
"realm_id": "01914b3c-1234-7f5a-b456-000000000001",
"client_id": "my-frontend",
"user_id": null,
"grant_type": "authorization_code",
"status": "pending",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 ...",
"started_at": "2026-10-30T14:30:00.000000Z",
"completed_at": null,
"duration_ms": null,
"steps": [
{
"id": "01914b3c-7e8a-7f5a-b456-789012345001",
"flow_id": "01914b3c-7e8a-7f5a-b456-789012345678",
"step_name": "authorize",
"status": "success",
"duration_ms": null,
"error_code": null,
"error_message": null,
"details": null,
"started_at": "2026-10-30T14:30:00.004000Z"
},
{
"id": "01914b3c-7e8a-7f5a-b456-789012345002",
"flow_id": "01914b3c-7e8a-7f5a-b456-789012345678",
"step_name": "credential_validation",
"status": "failure",
"duration_ms": 85,
"error_code": "InvalidPassword",
"error_message": null,
"details": null,
"started_at": "2026-10-30T14:30:07.511000Z"
}
]
}
}
The story is complete: the authorization request was accepted, then the password was wrong after 85 ms of verification. The flow is still pending because the user is on the login page and can try again.
Daily activity
GET /realms/{realm_name}/compass/v1/activity/daily
This endpoint aggregates per day and is the one that takes a date window.
| Parameter | Type | Description |
|---|---|---|
from | date, YYYY-MM-DD | First day, inclusive. Defaults to 29 days before to, which makes a 30-day window |
to | date, YYYY-MM-DD | Last day, inclusive. Defaults to the current UTC day |
client_id | string | Restrict the flow counts to one client |
user_id | UUID | Restrict the flow counts to one account |
grant_type | string | Restrict the flow counts to one grant type |
A malformed date, a from after to, or a window longer than 366 days returns 400. Unlike the flow listing, this endpoint ignores parameters it does not know, so a misspelled from_date silently falls back to the default window.
{
"data": [
{
"date": "2026-10-28",
"signups": 12,
"logins": 431,
"login_failures": 7,
"pending_logins": 3,
"expired_logins": 9,
"total_flows": 450,
"unique_login_users": 212,
"avg_login_duration_ms": 38.4
},
{
"date": "2026-10-29",
"signups": 9,
"logins": 402,
"login_failures": 5,
"pending_logins": 2,
"expired_logins": 11,
"total_flows": 420,
"unique_login_users": 198,
"avg_login_duration_ms": 36.9
},
{
"date": "2026-10-30",
"signups": 0,
"logins": 0,
"login_failures": 0,
"pending_logins": 0,
"expired_logins": 0,
"total_flows": 0,
"unique_login_users": 0,
"avg_login_duration_ms": null
}
]
}
Every day of the window gets a row, with zeros where nothing happened. Days are UTC days, taken from started_at.
| Field | Description |
|---|---|
signups | Accounts created that day in the realm. The filters do not apply to it |
logins | Flows with status success |
login_failures | Flows with status failure |
pending_logins | Flows still pending |
expired_logins | Flows with status expired |
total_flows | All flows that started that day |
unique_login_users | Distinct users among the successful flows |
avg_login_duration_ms | Average duration_ms of the successful flows. Null when there are none |
Queries people actually run
Authentication latency
Poll the stats endpoint on a schedule and graph avg_duration_ms:
curl -s "http://localhost:3333/realms/my-app/compass/v1/stats" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data | {total, failure_count, avg_duration_ms}'
Failed steps and their errors
Because a wrong password leaves its flow pending, count failed steps rather than failed flows. This groups the failed steps of recent pending flows by error code:
curl -s -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "status=pending" \
--data-urlencode "limit=100" \
-H "Authorization: Bearer $ADMIN_TOKEN" | \
jq '[.data[].steps[] | select(.status == "failure") | .error_code]
| group_by(.) | map({code: .[0], count: length}) | sort_by(-.count)'
The codes you will meet:
error_code | Step | Meaning |
|---|---|---|
InvalidPassword | credential_validation | Wrong password |
UserDisabled | credential_validation | The account is disabled |
AccountLocked | credential_validation | The account is locked after repeated failures |
invalid_otp_code | mfa_challenge | Wrong one-time code |
Failures at the token step carry the name of the internal error in the same way, and the SAML and token exchange steps carry their own error codes.
Finding slow sign-ins
Find successful flows where credential_validation, which covers the user lookup and the password hash check, took more than 500 ms:
curl -s -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "status=success" \
--data-urlencode "limit=100" \
-H "Authorization: Bearer $ADMIN_TOKEN" | \
jq '.data[] | select(.steps[] | select(.step_name == "credential_validation" and .duration_ms > 500))'
Identity provider performance
When you sign in through an external provider with Abyss, the idp_callback step is the one that waits on the provider’s token endpoint, so its duration_ms is the provider’s response time. A failure carries the provider’s own error and error_description in error_message. Filter on the broker grant type to see only these flows, for example grant_type=broker_github for a provider with the alias github.
A user’s history
curl -G "http://localhost:3333/realms/my-app/compass/v1/flows" \
--data-urlencode "user_id=USER_UUID" \
--data-urlencode "limit=20" \
-H "Authorization: Bearer $ADMIN_TOKEN"
This lists the user’s closed flows, which is useful for a support ticket (“when did I last sign in, and from where?”). It does not list their failed password attempts, because those flows are still anonymous. For those, match on ip_address or read the pending flows.
Keeping flows longer
Compass deletes flows 30 days after they started, and the window is not configurable. To keep a longer history, read flows out before they expire: page through GET .../flows with order_by=started_at, order=asc and a from that moves forward, and store them in your own system.
Compass and SeaWatch
The two modules complement each other.
| Aspect | SeaWatch | Compass |
|---|---|---|
| What it records | Security events: sign-ins, user and role changes, secret reads | Authentication flows, step by step |
| Granularity | One event per action | Several steps per flow |
| Timing | A timestamp | A duration per step and for the flow |
| Error detail | A status, and a reason on failed sign-ins | An error code and message per step |
| Network context | Not recorded | IP address and user agent, when the request carries them |
| Guarantees | Written with an awaited insert, chained, never deleted by FerrisKey | Best-effort: can drop events under load, deleted after 30 days, can be turned off per realm |
| Best for | The audit trail, “what happened” | Debugging, “where and why it went wrong” |
Use SeaWatch when you need to know what happened and be able to prove it. Use Compass when you need to know where a sign-in broke. To tie a SeaWatch event to its flow, see Querying events.