Build TimeTac
KINDAreplaces $7.2/mosaves $86.4/yrback to the verdict
A self-hosted working time record whose hard problems are an append-only history that survives a dispute, day arithmetic that stays right across breaks, overnight shifts and clock changes, and giving each worker readable access to their own record without letting anyone rewrite it.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the scheduled jobs.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v22 or higher - have readya few pounds a month
Why People clock in from phones, and a service on your laptop is a service that is off when you close it.
Get it A small virtual machine or a box on your network with a certificate.
Verify
A colleague clocks in from their phone without a certificate warning - decidefree to read; take advice if the answer is not obvious
Why It sets the retention period, what must be recorded and who may see it, and all three are structural rather than configurable afterwards.
Get it Read the rule for the country each worker is employed in, note the reference, the retention period and the date you checked.
Verify
The reference, period and date are stored in the database and shown in the interface - have readypennies per month at this size
Why Four years of evidence on one disk is four years of evidence away from one disk failure.
Get it Any offsite storage with a nightly copy and a retention longer than the record retention.
Verify
A restore drill into an empty instance reproduces totals and audit counts - decidefree
Why Introducing a time recording system is something people are entitled to be told about, and in several jurisdictions their representatives must be consulted.
Get it Write a short note covering what is recorded, who can see it, how long it is kept and how to request a correction.
Verify
The note is in the repository and linked from the application footer
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- users(id, name, email, password_hash, role, weekly_hours, holiday_days_per_year, started_at, ended_at) - entries(id, user_id, project_id, business_day, created_at, created_by, supersedes_id, superseded_at, void_reason) · a correction inserts a new row pointing at the old one - entry_versions(entry_id, start_at, end_at, break_minutes, note, source) · source is clock, manual or import - absences(id, user_id, kind, first_day, last_day, state, requested_at, decided_at, decided_by, supersedes_id) - approvals(user_id, period_month, approved_at, approved_by, locked) · a locked month refuses new versions without an explicit unlock - audit(id, at, actor_id, action, target_table, target_id, before_json, after_json, reason) · including actions the system took on its own - retention_policy(rule_reference, retention_years, checked_on) · the rule you followed and the date you checked it, in the database rather than in a comment - business_day is stored explicitly rather than derived from the start timestamp, because an overnight shift belongs to the day it started and no timezone maths will tell you that.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/hours.db | Entries, versions, absences, approvals and audit. Back this up nightly. |
SESSION_SECRETsecret | required | hex-64-chars-redacted | Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. |
ORG_TIMEZONE | required | Europe/Madrid | The zone business days and clock changes are computed in. |
RETENTION_YEARS | required | 4 | Set from the rule that applies to you, not from this example. |
RETENTION_RULE_REF | required | ET art 34.9, RDL 8/2019 | The rule you followed, stored beside the period and the date you checked it. |
BREAK_DEDUCTION_MINUTES | required | 30 | Applied at display time only. Changing it must not alter a stored time. |
BREAK_THRESHOLD_HOURS | required | 6 | Above this the deduction applies. Also display time only. |
The build, in order
An entry that cannot vanish
Clocking in and out records a time entry that can be corrected but never removed or overwritten.
The test that scans for them is the one that keeps the rule.
Files
src/db/queries.tstests/no-destructive-sql.test.tsThe reason is what an inspector or an employee will read.
Files
src/entries/correct.tsTwo open entries is an hours total nobody can defend.
Files
src/entries/clock.ts
done when · tick each as it passeswatch out- Adding a delete button because a test entry is cluttering the week is how the first genuine dispute becomes unanswerable, since the row somebody wants to see is the one that was tidied away. Void with a reason instead, keep the row, exclude voided entries from totals, and show them greyed in the week list so the correction is visible rather than hidden.
The day's arithmetic, including the awkward days
Totals are right for overnight shifts, unpaid breaks and the two days a year the clock changes.
An overnight shift belongs to the day it started.
Files
src/entries/day.tstests/overnight.test.tsA policy change must not rewrite last year.
Files
src/totals/policy.tsIt is two dates a year and it is always wrong first.
Files
tests/dst-totals.test.ts
done when · tick each as it passeswatch out- Writing the break-deducted total into the entry means changing the break policy silently rewrites history, and last quarter's approved timesheets no longer match the printed copies somebody already signed. Store the raw start, end and break minutes, apply the policy when displaying, and record the policy version alongside each approved month.
Approval, locking and an audit that names names
Approving a month locks it, and every change before or after is attributed and reversible in the record.
System actions are the ones people ask about.
Files
src/db/audited.tstests/system-audit.test.tsAn unlock with no reason is the same as no lock.
Files
src/approvals/lock.tsOne value tells you nothing in a dispute.
Files
src/audit/write.ts
done when · tick each as it passeswatch out- Writing audit rows from the interface layer means the background job that closes an abandoned open entry at midnight leaves no trace, and that is exactly the change somebody will query, because it is the one nobody remembers making. Write audit rows in the data layer so system actions are recorded with a system actor, and test that a job-created change produces one.
The worker's own copy
Each person can read their whole record, see every correction made to it, and request a change without editing anything.
The change is the part the worker needs to see.
Files
src/app/me/page.tsxSelf-service editing defeats the whole record.
Files
src/requests/correction.tsThe menu is not the security boundary.
Files
tests/cross-user-access.test.ts
done when · tick each as it passeswatch out- Showing the worker only the current version of each entry hides exactly the information the accessibility requirement is about, because the interesting fact is that the entry was changed and by whom. Show the version history inline with the reasons, and make a change made by somebody else visibly different from one the worker made.
Retention, export and a restore you have actually runproduct builder
The record survives four years or whatever your rule says, leaves in a format somebody else can read, and comes back from a backup you have tested.
The records outlive the employment by years.
Files
src/users/offboard.tstests/leaver-retention.test.tsA comment in the code is not a policy anybody can find.
Files
src/db/migrations/010_retention.sqlThe backup you have not restored is a directory.
Files
docs/restore-drill.md
done when · tick each as it passeswatch out- Treating a departing employee as a row to delete destroys exactly the records the retention rule requires you to keep, and it happens the week somebody leaves, long before anyone asks for them. End-date the user, remove contact details if you must, and keep entries, versions and audit rows for the full retention period with a test that proves they survive.
That is the whole plan for TimeTac. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Location, geofencing, screenshots and idle detection.
- Payroll and accounting integrations.
- Native mobile applications with offline clock-in.
- Country-specific overtime and accrual rule sets.
- Legally defensible records: no tamper-evident log, no retention guarantees, nothing an inspector or works council will nod at
- Native iOS and Android apps with GPS, offline clock-in and geofencing for people who are not at a desk
- Hardware terminals and badge or fingerprint clock-in for shop floors
- Payroll, ERP and HR integrations plus the export formats your bookkeeper already imports without complaining
- Someone else owning uptime, overtime rule edge cases and the shift and break math for each country
- A signed monthly statement each worker receives by email, which turns the record into something they have already seen
- An export shaped for one specific payroll system, written once your bookkeeper tells you which columns they want
- A yearly check that re-reads the working time rule and updates the stored reference and date
Need the files? The project pack on the verdict page hands your agent the whole brief · more time tracking.