Skip to content

Last Contacted Tracking

The last_contacted_at field on every contact record tracks when you last communicated — by message or call. It updates automatically and requires no manual input.

Outbound SMS sent ─┐
Inbound SMS received ─┤── tokio::spawn → update_contact_last_contacted(contact_id)
Inbound call started ─┘ │
contact.last_contacted_at = NOW()

Each trigger spawns an async task that updates the timestamp without blocking the main request. Errors are logged but never propagate — the primary operation (sending a message, receiving a call) always succeeds regardless of the tracking update.

src/mods/contact/services/update_contact_last_contacted_service.rs
pub async fn update_contact_last_contacted(
contact_id: Uuid,
) -> Result<(), AppError>

The service:

  1. Loads the contact by ID
  2. If not found (e.g. deleted between event and update), returns Ok(()) silently
  3. Sets last_contacted_at to chrono::Utc::now().fixed_offset()
  4. Saves the updated record

The service is called via tokio::spawn in three places:

TriggerFileWhen
Outbound SMSmessaging/api/send_message_api.rsAfter the message is persisted
Inbound SMStwilio/api/twilio_events_api.rsAfter the inbound message is saved
Inbound calltwilio/services/handle_twilio_stream_in_event_start.rsAfter the call record is inserted

Each call site follows the same pattern:

let contact_id_for_update = contact_id;
tokio::spawn(async move {
if let Err(e) = update_contact_last_contacted(contact_id_for_update).await {
tracing::warn!(error = %e, "Failed to update last_contacted_at");
}
});

When resolve_contact creates a new stub contact from an inbound call or message, it sets last_contacted_at to the current time immediately — so the contact is born with a valid timestamp.

Manually created contacts start with last_contacted_at: None until their first interaction.

The contacts list endpoint supports sorting by last contacted date:

GET /api/contacts/list?sort=last_contacted_at

This sorts contacts by most recently contacted first, with contacts that have never been contacted (NULL) appearing last. The sort uses a composite database index on (organization_id, last_contacted_at DESC NULLS LAST) for fast org-scoped queries.

Migration m20260302_000006_contact_add_last_contacted_at adds the column and backfills existing contacts:

  1. Adds last_contacted_at as a nullable TIMESTAMP WITH TIME ZONE column
  2. Backfills the value using GREATEST() across the most recent call and message timestamps for each contact
  3. Creates the composite index idx_contact_org_last_contacted