Skip to content

Member Activity Tracking

The last_active_at field on the member table tracks when each organization member last made an authenticated request. It updates automatically in the auth middleware with a 5-minute debounce and displays in the member list UI.

Any authenticated request
Auth middleware (get_auth_state_service)
Debounce check: last_active_at NULL or > 5 min old?
Yes ─┤── tokio::spawn → member.last_active_at = NOW()
No ─┘ (non-blocking, best-effort)

The update runs in a background task via tokio::spawn. It never blocks authentication and never causes request failures — errors are logged but silently ignored.

Migration m20260322_140001_member_add_last_active_at adds the column:

ALTER TABLE member ADD COLUMN last_active_at TIMESTAMPTZ NULL;

New members start with last_active_at: None. The first authenticated request populates it.

The debounce logic lives in src/bases/auth/services/get_auth_state_service.rs:

let needs_update = match member.last_active_at {
None => true,
Some(ref ts) => {
let elapsed = chrono::Utc::now()
.signed_duration_since(ts.with_timezone(&chrono::Utc));
elapsed.num_seconds() > 300 // 5 minutes
}
};
if needs_update {
let db = db.clone();
let member_clone = member.clone();
tokio::spawn(async move {
let mut active: schemas::member::ActiveModel = member_clone.into();
active.last_active_at = Set(Some(chrono::Utc::now().fixed_offset()));
if let Err(e) = active.update(&db).await {
tracing::error!(error = %e, "Failed to update member last_active_at");
}
});
}

Key properties:

  • Debounce threshold: 300 seconds (5 minutes) — at most one write per member per 5 minutes
  • Non-blocking: tokio::spawn decouples the update from the auth flow
  • Best-effort: Failures log an error but never propagate to the caller

GET /api/org/members-with-roles includes the field as an ISO 8601 string:

{
"member_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice Chen",
"email": "alice@example.com",
"is_owner": false,
"last_active_at": "2026-03-22 21:09:04.123456+00:00",
"roles": [],
"phones": []
}

Members who have never been active return "last_active_at": null.

The member list component (src/mods/settings/components/member_list_component.rs) renders the timestamp as a relative string using format_relative_time:

ValueDisplay
NoneNever
< 1 minutejust now
< 1 hour5m ago
< 1 day2h ago
< 1 week3d ago
< 1 month2w ago
< 1 year5mo ago
≥ 1 year>1y ago

Desktop: “Last Active” column in the members table. Mobile: “Active: 2h ago” text in the member card layout.

FilePurpose
migration/src/m20260322_140001_member_add_last_active_at.rsAdds last_active_at column
src/bases/auth/services/get_auth_state_service.rsDebounced update in auth middleware
src/bases/auth/services/create_member_service.rsInitializes new members with None
src/mods/settings/types/org_member_with_roles_type.rsOrgMemberWithRoles type definition
src/mods/settings/api/get_org_members_with_roles_api.rsAPI endpoint including the field
src/mods/settings/components/member_list_component.rsUI rendering
src/shared/utils/format_relative_time.rsRelative time formatting utility