Build TimeTac

KINDAreplaces $7.2/mosaves $86.4/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNext.js with TypeScript, self-hosted on a machine your team can reach over HTTPSStorageSQLite through a migration tool, one file, with no destructive update pathHistoryAppend-only entry revisions plus an audit table, never an in-place editAuthSigned session cookies with two roles, no third-party identity providerExportCSV and a printable monthly sheet per worker, generated from revisions

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/hours.dbEntries, versions, absences, approvals and audit. Back this up nightly.
SESSION_SECRETsecretrequiredhex-64-chars-redactedSigns session cookies. Rotating it logs everybody out, which is the intended behaviour.
ORG_TIMEZONErequiredEurope/MadridThe zone business days and clock changes are computed in.
RETENTION_YEARSrequired4Set from the rule that applies to you, not from this example.
RETENTION_RULE_REFrequiredET art 34.9, RDL 8/2019The rule you followed, stored beside the period and the date you checked it.
BREAK_DEDUCTION_MINUTESrequired30Applied at display time only. Changing it must not alter a stored time.
BREAK_THRESHOLD_HOURSrequired6Above this the deduction applies. Also display time only.

The build, in order

  1. An entry that cannot vanish

    Clocking in and out records a time entry that can be corrected but never removed or overwritten.

    1. The test that scans for them is the one that keeps the rule.

      Files src/db/queries.tstests/no-destructive-sql.test.ts

    2. The reason is what an inspector or an employee will read.

      Files src/entries/correct.ts

    3. Two open entries is an hours total nobody can defend.

      Files src/entries/clock.ts

    done when · tick each as it passes
    watch 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.
  2. 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.

    1. An overnight shift belongs to the day it started.

      Files src/entries/day.tstests/overnight.test.ts

    2. A policy change must not rewrite last year.

      Files src/totals/policy.ts

    3. It is two dates a year and it is always wrong first.

      Files tests/dst-totals.test.ts

    done when · tick each as it passes
    watch 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.
  3. 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.

    1. System actions are the ones people ask about.

      Files src/db/audited.tstests/system-audit.test.ts

    2. An unlock with no reason is the same as no lock.

      Files src/approvals/lock.ts

    3. One value tells you nothing in a dispute.

      Files src/audit/write.ts

    done when · tick each as it passes
    watch 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.
  4. 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.

    1. The change is the part the worker needs to see.

      Files src/app/me/page.tsx

    2. Self-service editing defeats the whole record.

      Files src/requests/correction.ts

    3. The menu is not the security boundary.

      Files tests/cross-user-access.test.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more time tracking.