---
type: SQLite Table
title: pay_rates
description: Hourly rate history per employee, keyed by the date it takes effect.
resource: backend/src/db/index.ts
tags: [data-model, sqlite, payroll]
timestamp: 2026-08-16T00:00:00Z
---

# pay_rates

Versioned hourly rate per employee. There's no `valid_to` column — the
active rate for a given date is simply the row with the largest
`valid_from <= that date` (`getRateAt` in `backend/src/services/payRates.ts`).
A new rate implicitly ends the previous one.

# Schema

| Column | Type | Description |
|---|---|---|
| `id` | INTEGER PK | autoincrement |
| `employee_id` | INTEGER | references [employees](./employees.md) |
| `hourly_rate` | REAL | Kč/hour |
| `valid_from` | TEXT | `YYYY-MM-DD`, takes effect from this date inclusive |
| `created_at` | TEXT | ISO 8601 UTC |

`UNIQUE (employee_id, valid_from)` — setting a rate again for the same date
overwrites it (`ON CONFLICT ... DO UPDATE`) rather than erroring.

# Behavior

- **First rate ever set for an employee** defaults `valid_from` to the
  employee's own `created_at` date when the admin doesn't specify one — so
  hours already worked before the admin got around to entering a rate
  still get paid correctly, rather than pricing at 0. This was a real bug
  found live: an admin set a rate today, and it didn't apply to shifts
  worked earlier that same month.
- **Every later rate change** (a raise) defaults to today instead — not
  retroactive. This asymmetry lives in `defaultValidFrom` in
  `backend/src/services/payRates.ts` and is covered by a test in
  `backend/src/services/payroll.test.ts`.

# Related

- [employees](./employees.md)
- [month_closures](./month-closures.md) / [payroll](./payroll.md) — consume
  `getRateAt` when computing `earned_estimate` / `base_amount`
