Companies building time tracking platforms are engineering workforce data infrastructure, not simple timer apps. The core system must handle concurrent clock-in events from thousands of field workers, enforce project and task hierarchies that map to client billing structures, calculate overtime correctly across multiple labour law jurisdictions (FLSA in the US, the EU Working Time Directive, state and provincial rules), and produce timesheets that payroll systems can consume without manual correction. The billing rate engine is typically more complex than it appears: rates vary by user, project, client, task type, and time period, and retroactive rate changes must produce corrected invoices without corrupting historical records. Whether building a professional services platform for agencies and consultancies, a field workforce management tool, or a developer-facing time tracker that integrates with project management systems, the engineering decisions in the time entry layer and rate engine determine whether the platform can scale to enterprise clients without bespoke configuration work. Scrums.com builds dedicated engineering teams that ship production-ready time tracking infrastructure in weeks, not quarters.
Time Tracking App Development
Build time tracking apps with Scrums.com. Teams for time entry, overtime calculation, billable rate engines, and payroll integration. Deploy in 21 days.
Time Entry Architecture and Clock-In/Out Infrastructure
The time_entries table is an event-sourced record: each entry has start_at, end_at (null while in progress), user_id, task_id, project_id, source (manual, timer, integration), and status (active, submitted, approved, locked). Never allow direct updates to submitted or approved entries: corrections create a new entry with an original_entry_id reference and a correction_reason. This append-only correction pattern preserves the full audit trail for payroll and billing disputes.
Concurrent clock-in prevention: a user can only have one active timer at a time. Enforce this with a partial unique index on (user_id, status = 'active') at the database level, not application-level checks, which are vulnerable to race conditions under concurrent requests. When a new timer starts, the active timer is automatically stopped (a clock-out event is recorded) before the new clock-in event is written, in a single transaction.
Idle detection sends a periodic heartbeat from the client; if no heartbeat is received within a configurable window (stored in idle_policy config), the server marks the timer as idle_suspected and records the last_active_at timestamp. On the user's return, the client presents the idle period for the user to either discard or attribute to a task. The idle period resolution (keep/discard) is recorded as a time entry correction, not an automatic mutation to the running timer.
GPS and geofence-based clock-in/out (for field and construction workforces) records location_events append-only. A geofence check at clock-in compares the user's location against a job_site_geofences config table. The check result (inside/outside, distance_metres) is stored alongside the clock event, never used to silently prevent clock-in. If a user clocks in outside a geofence, the event is recorded with an outside_geofence flag and routed to a manager review queue, not rejected.
Project and Task Hierarchy and Billable Rate Engine
The project/client/task hierarchy is a three-level tree: Client / Project / Task. Each node carries its own billing configuration: a billable_rates table with rate_type (hourly, daily, fixed, non_billable), amount (DECIMAL(10,4)), currency (ISO 4217), and effective_date ranges. Rate lookup at billing time uses the most specific applicable rate: Task rate overrides Project rate overrides Client rate overrides User default rate, all with effective_date range matching against the time_entry.start_at date, not the invoice date.
Budget tracking stores a project_budgets table: budget_type (hours or amount), budget_value, and period (total, monthly, weekly). A budget_consumption view computes spent_hours and spent_amount as a projection over approved time_entries, never as a stored counter, which requires careful increment/decrement logic around approvals, corrections, and rejections. Alerts fire when budget_consumption exceeds configurable thresholds (stored in budget_alert_policy): 75%, 90%, 100%.
The invoice projection queries time_entries filtered by (project_id, approved status, billing_period, not yet invoiced) and applies the billable_rates lookup to compute line items. invoice_line_items are generated and frozen at invoice creation time; they are not live views of the time_entries table, because the underlying rate or entry could change after invoice issuance. Once an invoice is created, the time_entries it covers are marked with invoice_id, which locks them from further correction.
Time-off and leave management integrates with the time entry system: approved leave entries (PTO, sick, holiday) flow into the same time_entries table with a leave_type flag, so utilisation and capacity reports account for planned absences without a separate data join.
Time tracking apps like these are built and delivered by dedicated engineering teams through our mobile app development service.
Timesheet Approval, Overtime, and Labour Law Compliance
Timesheet approval is a multi-stage workflow: draft, submitted, manager_approved, payroll_locked. Each stage transition is recorded as a timesheet_workflow_event with actor, timestamp, and any rejection reason. A rejected timesheet returns to draft with the rejection reason surfaced to the employee; the original submission is preserved in the workflow event log, not overwritten.
Overtime calculation is jurisdiction-dependent and must be driven by a labour_rules config table, not hardcoded. The minimum configurable parameters: daily_overtime_threshold (hours before OT kicks in: California is 8 hours; most other US states have no daily OT), weekly_overtime_threshold (FLSA: 40 hours), double_time_threshold (California: over 12 hours per day), and workweek_start_day (FLSA workweek start must be consistent and documented). EU Working Time Directive limits (48-hour average over 17 weeks, opt-out tracking) require a separate calculation path.
The overtime calculation runs as a projection over approved time_entries for the pay period, never stored as a field on the timesheet. If a manager approves or rejects entries after the initial calculation, the overtime figure recalculates automatically on next access. The payroll_locked stage freezes the calculation: once locked, no further changes are permitted without an unlock event recorded by a payroll administrator.
Payroll export adapters translate approved locked timesheets into the format expected by the payroll system (ADP Workforce Now, Gusto, Xero Payroll, Sage Payroll). Each adapter is independently versioned and maps time entry fields to payroll system fields via a payroll_mapping config table. The export is idempotent: re-exporting a locked timesheet produces identical output. The payroll_export_log records every export with export_id, payroll_system, pay_period, exported_at, exported_by, line_count, and a hash of the payload for integrity verification. Dedicated engineering teams from Scrums.com build these approval and payroll integration workflows to production reliability standards.
Integrations, Utilisation Analytics, and Project Profitability
Project management integrations (Jira, Asana, Linear, GitHub) use webhook receivers that create or update tasks in the time tracking system when issues are created or transitioned. The integration mapping is stored in integration_config: which issue types map to which task categories, which projects map to which client-project combinations, and which statuses constitute billable work. Webhooks are processed idempotently via a processed_webhooks deduplication table keyed on (source, external_event_id).
Git commit-triggered time logging: a GitHub Actions or GitLab CI hook fires on each commit, looks up the branch name against active tasks via a naming convention pattern stored in integration_config, and creates a time entry for the committing user. The entry is created with status = 'suggested' (not active) and surfaces in the user's pending entries for review, never auto-approved. The commit hash is stored on the time entry for traceability.
Utilisation reporting computes three metrics per user per period: billable_hours (entries with a billable rate), total_hours (all approved entries), and utilisation_rate (billable_hours / capacity_hours). Capacity is derived from the user's work_schedule config, accounting for public holidays in the user's jurisdiction and approved leave entries. These metrics are pre-computed by a nightly dbt job and stored in utilisation_snapshots, never computed live from the full time_entries table on each report request.
Project profitability reports join utilisation_snapshots with invoice_line_items and project_costs (salaries, tooling, overhead allocated by hours worked) to produce margin_amount and margin_percent per project. The cost allocation model is configurable (direct cost only, fully loaded, marginal) and stored in profitability_config, so methodology changes take effect without code deploys. Start a conversation with Scrums.com to get a dedicated team building this analytics infrastructure end to end.
Frequently Asked Questions
How do we prevent two timers running simultaneously for the same user?
Enforce a partial unique index on (user_id, status = 'active') at the database level, not application-level checks, which fail under concurrent requests. When a new timer starts, stop the active timer and start the new one in a single transaction. Application-level checks are a secondary defence; the database constraint is the definitive guard.
What is the correct pattern for handling overtime across US jurisdictions?
Store labour rules in a labour_rules config table with daily_overtime_threshold, weekly_overtime_threshold, double_time_threshold, and workweek_start_day per jurisdiction. The overtime calculation is a projection over approved time_entries for the pay period, not a stored field. If entries are added or corrected before the payroll_locked stage, the calculation recalculates automatically. Once locked, it is frozen and requires an explicit unlock event to change.
How do we handle retroactive rate changes without corrupting historical invoices?
Invoices freeze their line items at creation time using effective_date-range rate lookups based on the time entry's start_at date. Once an invoice is generated and time entries are marked with invoice_id, those entries are locked from correction. A retroactive rate change creates a new billable_rates row with a new effective_date: it does not modify existing rows and does not affect already-invoiced entries.
How should timesheet corrections be handled after manager approval?
Approved timesheets cannot be directly modified. Corrections create a new time_entry row with an original_entry_id reference and a correction_reason. The workflow returns to submitted status for re-approval. The timesheet_workflow_events log preserves the full history of every submission, approval, rejection, and correction with actor and timestamp.
How do we make payroll exports idempotent?
Each export writes a payroll_export_log row with a hash of the payload. Re-exporting a locked pay period hashes the current output and compares it against the log: identical payloads return without re-submission to the payroll system. Divergent hashes (indicating an attempted re-export after unlock and correction) require explicit confirmation before submission.
Build your Time Tracking app with Scrums.com
Build time tracking apps with Scrums.com. Teams for time entry, overtime calculation, billable rate engines, and payroll integration. Deploy in 21 days.
DEDICATED TEAMS · OPERATED DELIVERY · FIRST SPRINT IN 21 DAYS