Skip to main content

17. Hubstaff time-sync integration

Date: 2026-06-06

Status

Accepted

Context

In-house admin contractors' hours come from Hubstaff (real-time webhook primary, CSV import fallback). Members map to contractors, projects to facility assignments. Hubstaff's exact webhook signature scheme and payload shape could not be confirmed from their (JS-rendered) docs, and guessing security-critical verification or payload shapes risks accepting forged data or silently dropping real data.

Decision

  • Verification we control, fail-closed: the webhook (/api/webhooks/hubstaff) authenticates via a secret token in the URL (?token= or x-webhook-token), constant-time compared to HUBSTAFF_WEBHOOK_SECRET. Configure the Hubstaff webhook URL as …/api/webhooks/hubstaff?token=<secret>. No valid token → 401. This does not depend on Hubstaff's HMAC scheme and never fails open.
  • Tolerant, documented payload parsing: src/lib/hubstaff normalizeActivities accepts several plausible shapes (array, events/activities wrapper, single activity; member_id/user_id, project_id, date/starts_at, duration_seconds/tracked/duration) → normalized {externalId, memberId, projectId, date, hours}. Marked to reconcile against a real Hubstaff delivery. Pure + unit-tested.
  • Mapping + idempotent upsert: upsertSyncedEntries maps member → contractor (contractors.hubstaff_member_id), project → assignment (assignments.hubstaff_project_id, migration 0011), falling back to the active assignment. Upsert is idempotent on (contractor, assignment, date), marks source = 'hubstaff', clamps to 24h, and never overwrites a locked entry. Admin maps member ids (contractor detail) and project ids (assignment form).
  • CSV fallback (the reliable path now): parseCsv reads a defined header set (member_id, date, hours|time); the admin imports from /admin/contractors. Pure + unit-tested.

Consequences

  • The CSV path is fully usable and testable today; the webhook path is wired and secured but its payload mapping should be confirmed against one real Hubstaff delivery before relying on it (it fails safe until then — bad/unrecognized payloads simply produce zero applied entries).
  • In-house synced hours appear on the (read-only) contractor calendar alongside manual entries and feed the same pay engine.
  • The Hubstaff PAT carries an exp (~Jan 2027) and will need refreshing; it's stored in .env.local only.
  • A future enhancement could add HMAC body verification if Hubstaff's scheme is confirmed, and a REST "pull" sync using the PAT.