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.
How It Works
Section titled “How It Works”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.
Database
Section titled “Database”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.
Auth Middleware Integration
Section titled “Auth Middleware Integration”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::spawndecouples the update from the auth flow - Best-effort: Failures log an error but never propagate to the caller
API Response
Section titled “API Response”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.
UI Display
Section titled “UI Display”The member list component (src/mods/settings/components/member_list_component.rs) renders the timestamp as a relative string using format_relative_time:
| Value | Display |
|---|---|
None | Never |
| < 1 minute | just now |
| < 1 hour | 5m ago |
| < 1 day | 2h ago |
| < 1 week | 3d ago |
| < 1 month | 2w ago |
| < 1 year | 5mo ago |
| ≥ 1 year | >1y ago |
Desktop: “Last Active” column in the members table. Mobile: “Active: 2h ago” text in the member card layout.
Related Files
Section titled “Related Files”| File | Purpose |
|---|---|
migration/src/m20260322_140001_member_add_last_active_at.rs | Adds last_active_at column |
src/bases/auth/services/get_auth_state_service.rs | Debounced update in auth middleware |
src/bases/auth/services/create_member_service.rs | Initializes new members with None |
src/mods/settings/types/org_member_with_roles_type.rs | OrgMemberWithRoles type definition |
src/mods/settings/api/get_org_members_with_roles_api.rs | API endpoint including the field |
src/mods/settings/components/member_list_component.rs | UI rendering |
src/shared/utils/format_relative_time.rs | Relative time formatting utility |
Related Pages
Section titled “Related Pages”- Member Management — phone assignment, roles, and member removal
- Last Contacted Tracking — similar pattern for contact activity