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=orx-webhook-token), constant-time compared toHUBSTAFF_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/hubstaffnormalizeActivitiesaccepts several plausible shapes (array,events/activitieswrapper, singleactivity;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:
upsertSyncedEntriesmaps 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), markssource = 'hubstaff', clamps to 24h, and never overwrites alockedentry. Admin maps member ids (contractor detail) and project ids (assignment form). - CSV fallback (the reliable path now):
parseCsvreads 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.localonly. - A future enhancement could add HMAC body verification if Hubstaff's scheme is confirmed, and a REST "pull" sync using the PAT.