Rates & Billing
Bill rate vs. cost rate, override priority, currency snapshotting at write-time, the invoice bridge, retainer pools, and the Profitability tab.
Time Tracking maintains two rates per entry — a bill rate (what you charge the client) and a cost rate (what the work costs the company). Both are snapshotted on every time_entries row at write-time so retroactive rate changes never silently rewrite history.
The bill rate feeds invoices. The cost rate feeds profitability. Together they're the basis for margin reporting.
Bill rate vs. cost rate
| Rate | Meaning | Used for | Visibility |
|---|---|---|---|
hourly_rate (bill) | What you charge the client per hour | Invoice line items, billable amount in reports | Visible to the entry owner + workspace admins |
cost_rate (cost) | What the work costs you (loaded employee cost) | Profitability tab, margin calculations | Workspace admins + Time Pro feature gate |
currency | ISO 4217 (USD / EUR / GBP / …) | Currency on invoices, FX conversion at preview time | Snapshotted with both rates |
A new override row in time_rate_overrides can set both, just bill, or just cost. Whatever's set propagates onto every new entry the resolver picks up.
Override priority
When the rate resolver computes the rate for (user, project, task, log_date), it walks priorities in order, picks the first match. Most-specific wins:
- Task rate (
time_task_rates) — when the entry'sentity_type='task'and a task-rate override applies onlog_date - Override on
(user, project)— second most specific - Override on
(user, NULL)— applies to that user across all projects - Override on
(NULL, project)— applies to that project for any user - Workspace default (
time_tracking_settings.default_hourly_rate) — bill only; cost has no workspace-level fallback - Zero — no override + no default → 0
Within a bucket, the most recent applicable effective_from wins. An override applies on asOfDate if effective_from <= asOfDate AND (effective_to IS NULL OR asOfDate <= effective_to).
This means you can:
- Bump everyone's rate by setting
(NULL, NULL, $200, effective 2026-04-01)— applies to anyone on any project starting Apr 1. - Override one consultant on one client by setting
(alice, acme, $250, …)— wins over both her general rate and Acme's project rate. - Mark one specialised task at a premium rate (e.g. "after-hours migration work" task = $400/h) regardless of project + user.
- Open-ended date ranges (
effective_to = NULL) for "current rates"; close them with a date when you raise.
Per-task rate (5th level)
Configure on /time/settings/rates → bottom section. Add a row → pick the task, set rate + currency + effective range. Useful when one task within a project carries a higher rate than the rest of the project's work — without restructuring projects.
The 5th level is the most specific override. Once a task has a task-rate row, even a (user, project) override on the same task is bypassed. To exempt a user from a task rate, leave the task rate empty and rely on the user / project chain.
Snapshotting at write-time
Every time_entries row carries:
hourly_rate— bill rate at the time the entry was writtencost_rate— cost rate at the time the entry was written (nullable; null means "unknown")currency— currency at the time of write
These are immutable post-creation. Editing a rate override only affects future entries — historical reporting / billing stays correct.
When you start a timer, the snapshot happens at start. When you log a manual entry, at create. When the calendar import bridge converts a meeting, at convert.
The invoice bridge
Approved + billable + un-invoiced entries feed /finance/invoices/new → Pull from time:
- Open the invoice form.
- Click Pull from time in the toolbar.
- The invoice bridge previews lines grouped by your choice — entry / user / task / project. Each line shows hours × rate = amount, with currency.
- Confirm. Lines insert as
finance_invoice_line_itemsrows; the source entries getinvoice_line_idset so they don't show up in the next preview.
Currency consistency
The bridge rejects mixed-currency sets. If you select entries in USD and EUR, you get an error: pull each currency into its own invoice (or convert manually first). FX conversion in v1 is intentionally out of scope — too easy to introduce subtle bugs.
When all entries share a single currency, conversion is just sum(hours × rate) per group.
Voiding
Voiding an invoice (or deleting an invoice line) sets time_entries.invoice_line_id = NULL for the affected rows. They re-enter the un-invoiced pool and show up in the next preview.
Profitability (Pro)
The Profitability tab at /time/reports?tab=profitability is where bill rate × cost rate pays off. Per-project rows:
- Revenue —
Σ hours × hourly_ratefor billable + approved - Cost —
Σ hours × cost_ratefor entries withcost_rate IS NOT NULL - Expenses —
Σ amountfromexpenses_expensesrows tagged with the project - Margin = Revenue − Cost − Expenses
- Margin % = Margin / Revenue
Sorted by margin desc by default. Surfaces unprofitable projects immediately; a warning chip flags entries missing cost_rate (excluded from cost).
Project time budgets
Every project carries optional time-budget fields that drive the budget card on the project detail page and the daily budget-alerts cron:
| Field | Meaning |
|---|---|
time_budget_hours | Total hours the project is allowed to consume |
time_budget_alert_thresholds | Array of percentages that trigger alerts. Default [80, 100] — admins get notified at 80 % and again at 100 %. |
time_is_fixed_fee | When true, the project bills a flat fee instead of hours × rate |
time_fixed_fee_amount | Flat fee amount |
time_fixed_fee_currency | ISO 4217 currency for the fixed fee |
Set them under Project settings → Time on any project.
Burn-up chart
/projects/[id] → Budget tab includes a burn-up chart: cumulative hours tracked per day plotted against the budget line. Useful for spotting projects that started lean but are accelerating past their budget mid-cycle.
The chart pulls from approved + draft entries (so live work shows up); the budget card uses approved-only for the "official" actual.
Budget alerts
A daily cron at 16:45 UTC evaluates every project with time_budget_hours set:
- If
tracked / budget × 100 >= threshold, send an in-app notification to every workspace admin. - If
tracked > budget, also send an email — the over-budget signal is more urgent than approaching-budget.
Alerts are de-duplicated per (project × threshold × day) so admins don't get N alerts for the same threshold.
Fixed-fee projects
When a project's time_is_fixed_fee is true, the Profitability tab and the invoice bridge treat its revenue differently:
- Reports use
time_fixed_fee_amountas the revenue figure (instead ofΣ hours × rate) - The invoice bridge surfaces a Fixed fee option in the "Pull from time" preview — choose between billing actuals (hours × rate) or the contracted fee
- Margin calculations: revenue is the fixed fee, cost is still
Σ hours × cost_rate— useful for "are we losing money on this fixed-fee deal?"
The fixed-fee column appears on the Profitability tab when at least one project in the report is fixed-fee.
Recurring invoicing from time (Pro)
A finance_recurring_invoices row with source = 'time' automates the bridge: every period (weekly / monthly / quarterly), the cron sweeps approved + billable + un-invoiced hours into a fresh invoice draft. Configure at /finance/settings/recurring.
Retainer pools (Pro)
finance_retainers carries a contact's retainer balance: (hours_total, hours_used, period). When a time_entry on that contact is approved + billable, a hook decrements hours_used. The current balance is shown on the contact's right-rail, and on every new entry the composer shows "Acme: 7.5 / 20 retainer hours used this month".
When hours_used >= hours_total, new entries trip a soft warning ("Acme is over retainer — confirm to log"). Hard-block is configurable per retainer.
FX rates (future)
Mixed-currency invoicing isn't supported in v1. The roadmap includes:
- Daily ECB FX rates (table
fx_rates) - Per-line FX snapshot on
finance_invoice_line_items - Multi-currency invoice support with conversion at line creation time
Until then: pull each currency into its own invoice.
Where rates show up
| Surface | What it shows |
|---|---|
/time/settings/rates | Workspace-wide rate-override matrix (bill + cost columns) + per-task rates |
| Composer | The bill rate snapshot is shown on hover of the timer entity (informational) |
| Reports → Detailed tab | Rate column per row, Amount column = hours × rate |
| Reports → Profitability tab | Cost rate × hours = cost; revenue − cost = margin; fixed-fee projects use contracted amount as revenue |
/projects/[id] Budget tab | Tracked vs. budget hours, burn-up chart, fixed-fee status |
| Invoice bridge preview | Per-line rate + currency |
| CRM deal detail (right rail) | Projected cost (cost_rate) + projected margin if deal closes at current value |
Public REST API
| Endpoint | What |
|---|---|
GET /api/v1/time/rates | List overrides for the workspace |
POST /api/v1/time/rates | Upsert an override (create on no id, update on id) |
DELETE /api/v1/time/rates/[id] | Delete an override |
Bearer-auth scoped to the workspace.
Next steps
Approvals
Multi-level approval chains, period locking after final approval, notifications at every step. Single-level fallback for free workspaces.
Time Reports
Six tabs over the same dataset — Summary, Detailed, Weekly pivot, Profitability, Workload, Utilization. Plus the Custom Report Builder, saved views, scheduled email, and PDF / CSV export.