Vibecode TimeTac
track this build4 phases, 12 steps, beginner friendly0%If you are one person logging hours against projects, this is a solved evening: a table, a timer, a CSV export, done. TimeTac is not sold for that, it is sold to companies that must prove who worked when, which in Spain means a daily time record that survives an inspection. An agent can absolutely build the clock-in screen, the leave calendar and the monthly timesheet PDF, and it will feel surprisingly complete. What it will not hand you is the tamper-resistant audit trail, the payroll and HR exports your accountant already accepts, and the phone app your field staff will actually open. So: yes for yourself, shaky the moment other people's paychecks depend on your SQLite file.
You are building a lean indie version of TimeTac. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # TimeTac · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS | people clock in from a phone browser and the record has to be reachable | | Storage | SQLite through a migration tool, one file, with no destructive update path | one file is easy to back up and the absence of hard deletes is enforced in the schema | | History | Append-only entry revisions plus an audit table, never an in-place edit | a corrected timesheet has to show what it said before | | Auth | Signed session cookies with two roles, no third-party identity provider | a time record that depends on somebody else's login is a time record you can lose access to | | Export | CSV and a printable monthly sheet per worker, generated from revisions | the exports are what somebody outside your company will actually read | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the scheduled jobs. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A machine your team can reach over HTTPS** · a 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 - [ ] **The working time recording rule that applies to you** · free 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 - [ ] **Backup storage away from the machine** · pennies 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 - [ ] **What you tell staff before switching it on** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for TimeTac, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · TimeTac The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a team time record that employees can read, that nobody can quietly edit, and that exports in a shape payroll accepts to replace TimeTac. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero These records are evidence, and that decides the architecture rather than decorating it. In Case C-55/18, decided on 14 May 2019, the Court of Justice of the European Union held that Member States must require employers to set up an objective, reliable and accessible system enabling the duration of time worked each day by each worker to be measured. Spain implemented that in Royal Decree-Law 8/2019: article 34.9 of the Estatuto de los Trabajadores requires a daily record including the specific start and finish time of each worker's day, kept for four years and available to the workers, their legal representatives and the labour inspectorate. Other countries have their own rules and their own retention periods. Find the one that applies to you before choosing a retention setting, and write down which rule you followed and when you checked it. Three consequences you cannot retrofit. Nothing is ever hard deleted; a correction is a new fact with the old one still readable. Every change carries who made it, when, and both values. And the person whose hours these are can read their own record without asking an administrator, because accessible is in the ruling and because a record only management can see is not evidence in the worker's favour. Second rule: this is a record of working time, not a monitoring system. No location, no screenshots, no idle detection, no keystroke counting. Those turn a record people are entitled to into surveillance that in many jurisdictions requires notice, a lawful basis and consultation with staff representatives before you switch it on. If somebody asks for that feature, the answer is a written policy, not a field. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS · people clock in from a phone browser and the record has to be reachable - Storage: SQLite through a migration tool, one file, with no destructive update path · one file is easy to back up and the absence of hard deletes is enforced in the schema - History: Append-only entry revisions plus an audit table, never an in-place edit · a corrected timesheet has to show what it said before - Auth: Signed session cookies with two roles, no third-party identity provider · a time record that depends on somebody else's login is a time record you can lose access to - Export: CSV and a printable monthly sheet per worker, generated from revisions · the exports are what somebody outside your company will actually read ### Data model (create this before Phase 1) - 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. ### Phase 1 · An entry that cannot vanish Build: A clock in and clock out control showing today's running total, and a week list. A correction inserts a new entry version and a new entry row superseding the old one, with a reason. There is no update statement against a time value and no delete statement against an entry anywhere in the codebase. Reject a second open entry for the same person. Trap: 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. Done when: No SQL in the codebase updates a time value or deletes an entry, asserted by a test that scans the migrations and query layer. Correcting an entry creates a new version with a reason and leaves the previous version readable, tested. A second clock-in while an entry is open is refused with a message naming the open entry. A voided entry is excluded from totals and still visible in the week list, tested. Do not build yet: No day arithmetic, no approvals, no absences, no exports. ### Phase 2 · The day's arithmetic, including the awkward days Build: Compute daily and weekly totals from entry versions. Store the business day on the entry rather than deriving it, so an overnight shift counts on the day it began. Apply a configurable unpaid break deduction at display time from the stored raw times, never by writing the deducted value. Test the arithmetic across a clock change in your own zone. Trap: 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. Done when: A shift starting at 22:00 and ending at 06:00 counts eight hours on the day it started, tested. Totals across a spring-forward and an autumn-back date in the configured zone are correct, tested with fixture dates for both. Changing the break policy does not alter any stored time value, verified in the database before and after. An approved month records the policy version it was approved under. Do not build yet: No approvals, no absences, no self-service, no exports. ### Phase 3 · Approval, locking and an audit that names names Build: An administrator view of a month per person with one-click approval that locks the month. A locked month refuses new entry versions unless an administrator explicitly unlocks it, and the unlock is itself an audit row with a reason. Every create, correct, void, approve and unlock writes an audit row with the actor, both values and the reason. Trap: 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. Done when: A locked month refuses a new entry version and the refusal names the lock, tested. An unlock writes an audit row with an actor and a reason and is visible in the month view, tested. A change made by a scheduled job produces an audit row with a system actor, tested by running the job. Audit rows carry before and after values for every field that changed, checked against a correction. Do not build yet: No self-service access, no absences, no exports. ### Phase 4 · The worker's own copy Build: A personal view of any period showing entries, versions, corrections with reasons and who made them, plus running balances against contracted hours and holiday. A correction request is a record the worker creates and an administrator acts on, never a direct edit. Absence requests follow the same shape: requested, decided, both recorded. Trap: 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. Done when: A worker can view every version of their own entries with the actor and the reason for each change, tested through the interface. A worker cannot read another worker's record through any route, tested by requesting one directly. A correction request creates a record and never modifies an entry, asserted in the suite. Balances are computed from versions and match the administrator's view for the same period, tested. Do not build yet: No exports, no retention handling, no backup drill. ### Phase 5 · Retention, export and a restore you have actually run Build: Store the retention rule reference, the period and the date you checked it in the database, and show them in the interface. Export entries, versions, absences, approvals and audit rows as CSV, plus a printable monthly sheet per person with signature lines. Nightly backups with a restore drill you run and document. Deleting a leaver removes contact details and keeps the time record. Trap: 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. Done when: Ending a user's employment keeps their entries, versions and audit rows and they still appear in exports for the retention period, tested. The retention rule reference, period and check date are stored and displayed, and the application warns when the check date is older than a year. Export produces every table as CSV and the monthly sheet matches the on-screen totals for the same period, tested. A restore from the nightly backup into an empty instance reproduces totals and audit counts, documented with the date you ran it. Do not build yet: Nothing after this. ### Out of scope (and why) - Location, geofencing, screenshots, idle detection and keystroke counting, which change this from a record into monitoring and carry notice, lawful basis and staff consultation obligations in many places. - Payroll and accounting integrations, because the value there is that your bookkeeper's system already accepts a specific file, and matching it is a per-customer contract rather than a feature. - Native mobile applications with offline clock-in, which is a genuine reason field teams pay, and a browser page on a phone is not the same thing. - Country-specific overtime, break and absence accrual rule sets, which are the actual product being sold and cannot be guessed at from the outside. ### README must contain - Which working time rule you are following, its retention period, and the date you checked it - That nothing is ever hard deleted, that corrections are new versions, and where the audit trail lives - That every worker can read their own full record including corrections, and how they request a change - That this is a record and not a monitoring tool, and the specific features refused on that basis - The backup schedule, the restore drill and the date it was last run successfully ===== AGENTS.md ===== # Agent instructions · TimeTac indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS, SQLite through a migration tool, one file, with no destructive update path, Append-only entry revisions plus an audit table, never an in-place edit, Signed session cookies with two roles, no third-party identity provider, CSV and a printable monthly sheet per worker, generated from revisions. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · TimeTac 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · An entry that cannot vanish Clocking in and out records a time entry that can be corrected but never removed or overwritten. ### Steps 1. Forbid update and delete on time values The test that scans for them is the one that keeps the rule. Files: `src/db/queries.ts`, `tests/no-destructive-sql.test.ts` 2. Make a correction a new version with a reason The reason is what an inspector or an employee will read. Files: `src/entries/correct.ts` 3. Refuse a second open entry Two open entries is an hours total nobody can defend. Files: `src/entries/clock.ts` ### Done when - [ ] No update or delete touches a time value - [ ] Corrections create versions with reasons - [ ] A second open entry is refused - [ ] Voided entries leave totals but stay visible ### 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. ## Phase 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. ### Steps 1. Store the business day, do not derive it An overnight shift belongs to the day it started. Files: `src/entries/day.ts`, `tests/overnight.test.ts` 2. Deduct breaks at display time only A policy change must not rewrite last year. Files: `src/totals/policy.ts` 3. Test both clock changes in your own zone It is two dates a year and it is always wrong first. Files: `tests/dst-totals.test.ts` ### Done when - [ ] An overnight shift counts on its start day - [ ] Totals are correct across both clock changes - [ ] A policy change alters no stored time - [ ] Approved months record their policy version ### 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. ## Phase 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. ### Steps 1. Write audit rows in the data layer System actions are the ones people ask about. Files: `src/db/audited.ts`, `tests/system-audit.test.ts` 2. Lock on approval, unlock with a reason An unlock with no reason is the same as no lock. Files: `src/approvals/lock.ts` 3. Record before and after on every change One value tells you nothing in a dispute. Files: `src/audit/write.ts` ### Done when - [ ] A locked month refuses new versions - [ ] Unlocks carry an actor and a reason - [ ] Scheduled job changes are audited - [ ] Audit rows carry before and after values ### 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. ## Phase 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. ### Steps 1. Show version history inline, with reasons The change is the part the worker needs to see. Files: `src/app/me/page.tsx` 2. Make a correction a request, never an edit Self-service editing defeats the whole record. Files: `src/requests/correction.ts` 3. Test cross-user access directly, not through the menu The menu is not the security boundary. Files: `tests/cross-user-access.test.ts` ### Done when - [ ] Workers see every version with actor and reason - [ ] Cross-user record access is refused - [ ] Correction requests modify no entries - [ ] Personal and administrator balances agree ### 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. ## Not in this build - Location, geofencing, screenshots and idle detection. - Payroll and accounting integrations. - Native mobile applications with offline clock-in. - Country-specific overtime and accrual rule sets. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Entries, versions, absences, approvals and audit. Back this up nightly. DATABASE_PATH=./data/hours.db # Required · secret. Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. SESSION_SECRET=hex-64-chars-redacted # Required. The zone business days and clock changes are computed in. ORG_TIMEZONE=Europe/Madrid # Required. Set from the rule that applies to you, not from this example. RETENTION_YEARS=4 # Required. The rule you followed, stored beside the period and the date you checked it. RETENTION_RULE_REF=ET art 34.9, RDL 8/2019 # Required. Applied at display time only. Changing it must not alter a stored time. BREAK_DEDUCTION_MINUTES=30 # Required. Above this the deduction applies. Also display time only. BREAK_THRESHOLD_HOURS=6
You are building a lean indie version of TimeTac. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # TimeTac · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS | people clock in from a phone browser and the record has to be reachable | | Storage | SQLite through a migration tool, one file, with no destructive update path | one file is easy to back up and the absence of hard deletes is enforced in the schema | | History | Append-only entry revisions plus an audit table, never an in-place edit | a corrected timesheet has to show what it said before | | Auth | Signed session cookies with two roles, no third-party identity provider | a time record that depends on somebody else's login is a time record you can lose access to | | Export | CSV and a printable monthly sheet per worker, generated from revisions | the exports are what somebody outside your company will actually read | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the scheduled jobs. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A machine your team can reach over HTTPS** · a 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 - [ ] **The working time recording rule that applies to you** · free 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 - [ ] **Backup storage away from the machine** · pennies 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 - [ ] **What you tell staff before switching it on** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for TimeTac, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · TimeTac The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a team time record that employees can read, that nobody can quietly edit, and that exports in a shape payroll accepts to replace TimeTac. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero These records are evidence, and that decides the architecture rather than decorating it. In Case C-55/18, decided on 14 May 2019, the Court of Justice of the European Union held that Member States must require employers to set up an objective, reliable and accessible system enabling the duration of time worked each day by each worker to be measured. Spain implemented that in Royal Decree-Law 8/2019: article 34.9 of the Estatuto de los Trabajadores requires a daily record including the specific start and finish time of each worker's day, kept for four years and available to the workers, their legal representatives and the labour inspectorate. Other countries have their own rules and their own retention periods. Find the one that applies to you before choosing a retention setting, and write down which rule you followed and when you checked it. Three consequences you cannot retrofit. Nothing is ever hard deleted; a correction is a new fact with the old one still readable. Every change carries who made it, when, and both values. And the person whose hours these are can read their own record without asking an administrator, because accessible is in the ruling and because a record only management can see is not evidence in the worker's favour. Second rule: this is a record of working time, not a monitoring system. No location, no screenshots, no idle detection, no keystroke counting. Those turn a record people are entitled to into surveillance that in many jurisdictions requires notice, a lawful basis and consultation with staff representatives before you switch it on. If somebody asks for that feature, the answer is a written policy, not a field. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS · people clock in from a phone browser and the record has to be reachable - Storage: SQLite through a migration tool, one file, with no destructive update path · one file is easy to back up and the absence of hard deletes is enforced in the schema - History: Append-only entry revisions plus an audit table, never an in-place edit · a corrected timesheet has to show what it said before - Auth: Signed session cookies with two roles, no third-party identity provider · a time record that depends on somebody else's login is a time record you can lose access to - Export: CSV and a printable monthly sheet per worker, generated from revisions · the exports are what somebody outside your company will actually read ### Data model (create this before Phase 1) - 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. ### Phase 1 · An entry that cannot vanish Build: A clock in and clock out control showing today's running total, and a week list. A correction inserts a new entry version and a new entry row superseding the old one, with a reason. There is no update statement against a time value and no delete statement against an entry anywhere in the codebase. Reject a second open entry for the same person. Trap: 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. Done when: No SQL in the codebase updates a time value or deletes an entry, asserted by a test that scans the migrations and query layer. Correcting an entry creates a new version with a reason and leaves the previous version readable, tested. A second clock-in while an entry is open is refused with a message naming the open entry. A voided entry is excluded from totals and still visible in the week list, tested. Do not build yet: No day arithmetic, no approvals, no absences, no exports. ### Phase 2 · The day's arithmetic, including the awkward days Build: Compute daily and weekly totals from entry versions. Store the business day on the entry rather than deriving it, so an overnight shift counts on the day it began. Apply a configurable unpaid break deduction at display time from the stored raw times, never by writing the deducted value. Test the arithmetic across a clock change in your own zone. Trap: 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. Done when: A shift starting at 22:00 and ending at 06:00 counts eight hours on the day it started, tested. Totals across a spring-forward and an autumn-back date in the configured zone are correct, tested with fixture dates for both. Changing the break policy does not alter any stored time value, verified in the database before and after. An approved month records the policy version it was approved under. Do not build yet: No approvals, no absences, no self-service, no exports. ### Phase 3 · Approval, locking and an audit that names names Build: An administrator view of a month per person with one-click approval that locks the month. A locked month refuses new entry versions unless an administrator explicitly unlocks it, and the unlock is itself an audit row with a reason. Every create, correct, void, approve and unlock writes an audit row with the actor, both values and the reason. Trap: 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. Done when: A locked month refuses a new entry version and the refusal names the lock, tested. An unlock writes an audit row with an actor and a reason and is visible in the month view, tested. A change made by a scheduled job produces an audit row with a system actor, tested by running the job. Audit rows carry before and after values for every field that changed, checked against a correction. Do not build yet: No self-service access, no absences, no exports. ### Phase 4 · The worker's own copy Build: A personal view of any period showing entries, versions, corrections with reasons and who made them, plus running balances against contracted hours and holiday. A correction request is a record the worker creates and an administrator acts on, never a direct edit. Absence requests follow the same shape: requested, decided, both recorded. Trap: 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. Done when: A worker can view every version of their own entries with the actor and the reason for each change, tested through the interface. A worker cannot read another worker's record through any route, tested by requesting one directly. A correction request creates a record and never modifies an entry, asserted in the suite. Balances are computed from versions and match the administrator's view for the same period, tested. Do not build yet: No exports, no retention handling, no backup drill. ### Phase 5 · Retention, export and a restore you have actually run Build: Store the retention rule reference, the period and the date you checked it in the database, and show them in the interface. Export entries, versions, absences, approvals and audit rows as CSV, plus a printable monthly sheet per person with signature lines. Nightly backups with a restore drill you run and document. Deleting a leaver removes contact details and keeps the time record. Trap: 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. Done when: Ending a user's employment keeps their entries, versions and audit rows and they still appear in exports for the retention period, tested. The retention rule reference, period and check date are stored and displayed, and the application warns when the check date is older than a year. Export produces every table as CSV and the monthly sheet matches the on-screen totals for the same period, tested. A restore from the nightly backup into an empty instance reproduces totals and audit counts, documented with the date you ran it. Do not build yet: Nothing after this. ### Out of scope (and why) - Location, geofencing, screenshots, idle detection and keystroke counting, which change this from a record into monitoring and carry notice, lawful basis and staff consultation obligations in many places. - Payroll and accounting integrations, because the value there is that your bookkeeper's system already accepts a specific file, and matching it is a per-customer contract rather than a feature. - Native mobile applications with offline clock-in, which is a genuine reason field teams pay, and a browser page on a phone is not the same thing. - Country-specific overtime, break and absence accrual rule sets, which are the actual product being sold and cannot be guessed at from the outside. ### README must contain - Which working time rule you are following, its retention period, and the date you checked it - That nothing is ever hard deleted, that corrections are new versions, and where the audit trail lives - That every worker can read their own full record including corrections, and how they request a change - That this is a record and not a monitoring tool, and the specific features refused on that basis - The backup schedule, the restore drill and the date it was last run successfully ===== AGENTS.md ===== # Agent instructions · TimeTac indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS, SQLite through a migration tool, one file, with no destructive update path, Append-only entry revisions plus an audit table, never an in-place edit, Signed session cookies with two roles, no third-party identity provider, CSV and a printable monthly sheet per worker, generated from revisions. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · TimeTac 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · An entry that cannot vanish Clocking in and out records a time entry that can be corrected but never removed or overwritten. ### Steps 1. Forbid update and delete on time values The test that scans for them is the one that keeps the rule. Files: `src/db/queries.ts`, `tests/no-destructive-sql.test.ts` 2. Make a correction a new version with a reason The reason is what an inspector or an employee will read. Files: `src/entries/correct.ts` 3. Refuse a second open entry Two open entries is an hours total nobody can defend. Files: `src/entries/clock.ts` ### Done when - [ ] No update or delete touches a time value - [ ] Corrections create versions with reasons - [ ] A second open entry is refused - [ ] Voided entries leave totals but stay visible ### 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. ## Phase 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. ### Steps 1. Store the business day, do not derive it An overnight shift belongs to the day it started. Files: `src/entries/day.ts`, `tests/overnight.test.ts` 2. Deduct breaks at display time only A policy change must not rewrite last year. Files: `src/totals/policy.ts` 3. Test both clock changes in your own zone It is two dates a year and it is always wrong first. Files: `tests/dst-totals.test.ts` ### Done when - [ ] An overnight shift counts on its start day - [ ] Totals are correct across both clock changes - [ ] A policy change alters no stored time - [ ] Approved months record their policy version ### 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. ## Phase 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. ### Steps 1. Write audit rows in the data layer System actions are the ones people ask about. Files: `src/db/audited.ts`, `tests/system-audit.test.ts` 2. Lock on approval, unlock with a reason An unlock with no reason is the same as no lock. Files: `src/approvals/lock.ts` 3. Record before and after on every change One value tells you nothing in a dispute. Files: `src/audit/write.ts` ### Done when - [ ] A locked month refuses new versions - [ ] Unlocks carry an actor and a reason - [ ] Scheduled job changes are audited - [ ] Audit rows carry before and after values ### 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. ## Phase 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. ### Steps 1. Show version history inline, with reasons The change is the part the worker needs to see. Files: `src/app/me/page.tsx` 2. Make a correction a request, never an edit Self-service editing defeats the whole record. Files: `src/requests/correction.ts` 3. Test cross-user access directly, not through the menu The menu is not the security boundary. Files: `tests/cross-user-access.test.ts` ### Done when - [ ] Workers see every version with actor and reason - [ ] Cross-user record access is refused - [ ] Correction requests modify no entries - [ ] Personal and administrator balances agree ### 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. ## Not in this build - Location, geofencing, screenshots and idle detection. - Payroll and accounting integrations. - Native mobile applications with offline clock-in. - Country-specific overtime and accrual rule sets. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Entries, versions, absences, approvals and audit. Back this up nightly. DATABASE_PATH=./data/hours.db # Required · secret. Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. SESSION_SECRET=hex-64-chars-redacted # Required. The zone business days and clock changes are computed in. ORG_TIMEZONE=Europe/Madrid # Required. Set from the rule that applies to you, not from this example. RETENTION_YEARS=4 # Required. The rule you followed, stored beside the period and the date you checked it. RETENTION_RULE_REF=ET art 34.9, RDL 8/2019 # Required. Applied at display time only. Changing it must not alter a stored time. BREAK_DEDUCTION_MINUTES=30 # Required. Above this the deduction applies. Also display time only. BREAK_THRESHOLD_HOURS=6
You are building a production product version of TimeTac. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # TimeTac · product brief ## Problem If you are one person logging hours against projects, this is a solved evening: a table, a timer, a CSV export, done. TimeTac is not sold for that, it is sold to companies that must prove who worked when, which in Spain means a daily time record that survives an inspection. An agent can absolutely build the clock-in screen, the leave calendar and the monthly timesheet PDF, and it will feel surprisingly complete. What it will not hand you is the tamper-resistant audit trail, the payroll and HR exports your accountant already accepts, and the phone app your field staff will actually open. So: yes for yourself, shaky the moment other people's paychecks depend on your SQLite file. ## Product outcome A working time record where corrections are visible rather than silent, every worker can read their own history, a month can be locked and only deliberately unlocked, and four years of evidence survive a departing employee and a dead disk. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20+ - somewhere to run it that your team can reach, plus HTTPS if it leaves your laptop - backups you actually test, because these records are the evidence ## Explicit non-goals for v1 - 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 ## Success criteria - No query is confirmed to update or delete a time value - An overnight shift is confirmed to count on its start day - A scheduled job change is confirmed to produce an audit row - A worker is confirmed able to read every version of their own entries - A leaver is confirmed to keep entries, versions and audit rows ===== BRIEF.md ===== # Build brief · TimeTac The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a team time record that employees can read, that nobody can quietly edit, and that exports in a shape payroll accepts to replace TimeTac. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero These records are evidence, and that decides the architecture rather than decorating it. In Case C-55/18, decided on 14 May 2019, the Court of Justice of the European Union held that Member States must require employers to set up an objective, reliable and accessible system enabling the duration of time worked each day by each worker to be measured. Spain implemented that in Royal Decree-Law 8/2019: article 34.9 of the Estatuto de los Trabajadores requires a daily record including the specific start and finish time of each worker's day, kept for four years and available to the workers, their legal representatives and the labour inspectorate. Other countries have their own rules and their own retention periods. Find the one that applies to you before choosing a retention setting, and write down which rule you followed and when you checked it. Three consequences you cannot retrofit. Nothing is ever hard deleted; a correction is a new fact with the old one still readable. Every change carries who made it, when, and both values. And the person whose hours these are can read their own record without asking an administrator, because accessible is in the ruling and because a record only management can see is not evidence in the worker's favour. Second rule: this is a record of working time, not a monitoring system. No location, no screenshots, no idle detection, no keystroke counting. Those turn a record people are entitled to into surveillance that in many jurisdictions requires notice, a lawful basis and consultation with staff representatives before you switch it on. If somebody asks for that feature, the answer is a written policy, not a field. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS · people clock in from a phone browser and the record has to be reachable - Storage: SQLite through a migration tool, one file, with no destructive update path · one file is easy to back up and the absence of hard deletes is enforced in the schema - History: Append-only entry revisions plus an audit table, never an in-place edit · a corrected timesheet has to show what it said before - Auth: Signed session cookies with two roles, no third-party identity provider · a time record that depends on somebody else's login is a time record you can lose access to - Export: CSV and a printable monthly sheet per worker, generated from revisions · the exports are what somebody outside your company will actually read ### Data model (create this before Phase 1) - 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. ### Phase 1 · An entry that cannot vanish Build: A clock in and clock out control showing today's running total, and a week list. A correction inserts a new entry version and a new entry row superseding the old one, with a reason. There is no update statement against a time value and no delete statement against an entry anywhere in the codebase. Reject a second open entry for the same person. Trap: 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. Done when: No SQL in the codebase updates a time value or deletes an entry, asserted by a test that scans the migrations and query layer. Correcting an entry creates a new version with a reason and leaves the previous version readable, tested. A second clock-in while an entry is open is refused with a message naming the open entry. A voided entry is excluded from totals and still visible in the week list, tested. Do not build yet: No day arithmetic, no approvals, no absences, no exports. ### Phase 2 · The day's arithmetic, including the awkward days Build: Compute daily and weekly totals from entry versions. Store the business day on the entry rather than deriving it, so an overnight shift counts on the day it began. Apply a configurable unpaid break deduction at display time from the stored raw times, never by writing the deducted value. Test the arithmetic across a clock change in your own zone. Trap: 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. Done when: A shift starting at 22:00 and ending at 06:00 counts eight hours on the day it started, tested. Totals across a spring-forward and an autumn-back date in the configured zone are correct, tested with fixture dates for both. Changing the break policy does not alter any stored time value, verified in the database before and after. An approved month records the policy version it was approved under. Do not build yet: No approvals, no absences, no self-service, no exports. ### Phase 3 · Approval, locking and an audit that names names Build: An administrator view of a month per person with one-click approval that locks the month. A locked month refuses new entry versions unless an administrator explicitly unlocks it, and the unlock is itself an audit row with a reason. Every create, correct, void, approve and unlock writes an audit row with the actor, both values and the reason. Trap: 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. Done when: A locked month refuses a new entry version and the refusal names the lock, tested. An unlock writes an audit row with an actor and a reason and is visible in the month view, tested. A change made by a scheduled job produces an audit row with a system actor, tested by running the job. Audit rows carry before and after values for every field that changed, checked against a correction. Do not build yet: No self-service access, no absences, no exports. ### Phase 4 · The worker's own copy Build: A personal view of any period showing entries, versions, corrections with reasons and who made them, plus running balances against contracted hours and holiday. A correction request is a record the worker creates and an administrator acts on, never a direct edit. Absence requests follow the same shape: requested, decided, both recorded. Trap: 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. Done when: A worker can view every version of their own entries with the actor and the reason for each change, tested through the interface. A worker cannot read another worker's record through any route, tested by requesting one directly. A correction request creates a record and never modifies an entry, asserted in the suite. Balances are computed from versions and match the administrator's view for the same period, tested. Do not build yet: No exports, no retention handling, no backup drill. ### Phase 5 · Retention, export and a restore you have actually run Build: Store the retention rule reference, the period and the date you checked it in the database, and show them in the interface. Export entries, versions, absences, approvals and audit rows as CSV, plus a printable monthly sheet per person with signature lines. Nightly backups with a restore drill you run and document. Deleting a leaver removes contact details and keeps the time record. Trap: 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. Done when: Ending a user's employment keeps their entries, versions and audit rows and they still appear in exports for the retention period, tested. The retention rule reference, period and check date are stored and displayed, and the application warns when the check date is older than a year. Export produces every table as CSV and the monthly sheet matches the on-screen totals for the same period, tested. A restore from the nightly backup into an empty instance reproduces totals and audit counts, documented with the date you ran it. Do not build yet: Nothing after this. ### Out of scope (and why) - Location, geofencing, screenshots, idle detection and keystroke counting, which change this from a record into monitoring and carry notice, lawful basis and staff consultation obligations in many places. - Payroll and accounting integrations, because the value there is that your bookkeeper's system already accepts a specific file, and matching it is a per-customer contract rather than a feature. - Native mobile applications with offline clock-in, which is a genuine reason field teams pay, and a browser page on a phone is not the same thing. - Country-specific overtime, break and absence accrual rule sets, which are the actual product being sold and cannot be guessed at from the outside. ### README must contain - Which working time rule you are following, its retention period, and the date you checked it - That nothing is ever hard deleted, that corrections are new versions, and where the audit trail lives - That every worker can read their own full record including corrections, and how they request a change - That this is a record and not a monitoring tool, and the specific features refused on that basis - The backup schedule, the restore drill and the date it was last run successfully ===== ARCHITECTURE.md ===== # Architecture · TimeTac ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS | people clock in from a phone browser and the record has to be reachable | | Storage | SQLite through a migration tool, one file, with no destructive update path | one file is easy to back up and the absence of hard deletes is enforced in the schema | | History | Append-only entry revisions plus an audit table, never an in-place edit | a corrected timesheet has to show what it said before | | Auth | Signed session cookies with two roles, no third-party identity provider | a time record that depends on somebody else's login is a time record you can lose access to | | Export | CSV and a printable monthly sheet per worker, generated from revisions | the exports are what somebody outside your company will actually read | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Entries | Clocking, versions, voids and the no-delete rule. | Any store, provided no query updates or deletes a time value. | | Totals | Business days, overnight shifts and display-time break policy. | Any implementation, provided the policy is never written into stored times. | | Approvals | Month locking, unlocks with reasons and policy versions. | Any workflow, provided an unlock is audited as loudly as an approval. | | Audit | Data-layer attribution including system actions. | Not swappable for interface-layer logging, which misses the job at midnight. | | Retention | The rule reference, leaver handling and exports. | Any policy, provided leavers keep their records for the full period. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Entries, versions, absences, approvals and audit. Back this up nightly. - `SESSION_SECRET` · required, secret · Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. - `ORG_TIMEZONE` · required · The zone business days and clock changes are computed in. - `RETENTION_YEARS` · required · Set from the rule that applies to you, not from this example. - `RETENTION_RULE_REF` · required · The rule you followed, stored beside the period and the date you checked it. - `BREAK_DEDUCTION_MINUTES` · required · Applied at display time only. Changing it must not alter a stored time. - `BREAK_THRESHOLD_HOURS` · required · Above this the deduction applies. Also display time only. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · TimeTac product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS, SQLite through a migration tool, one file, with no destructive update path, Append-only entry revisions plus an audit table, never an in-place edit, Signed session cookies with two roles, no third-party identity provider, CSV and a printable monthly sheet per worker, generated from revisions. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · TimeTac Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · An entry that cannot vanish Clocking in and out records a time entry that can be corrected but never removed or overwritten. ### Steps 1. Forbid update and delete on time values The test that scans for them is the one that keeps the rule. Files: `src/db/queries.ts`, `tests/no-destructive-sql.test.ts` 2. Make a correction a new version with a reason The reason is what an inspector or an employee will read. Files: `src/entries/correct.ts` 3. Refuse a second open entry Two open entries is an hours total nobody can defend. Files: `src/entries/clock.ts` ### Done when - [ ] No update or delete touches a time value - [ ] Corrections create versions with reasons - [ ] A second open entry is refused - [ ] Voided entries leave totals but stay visible ### 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. ## M2 · 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. ### Steps 1. Store the business day, do not derive it An overnight shift belongs to the day it started. Files: `src/entries/day.ts`, `tests/overnight.test.ts` 2. Deduct breaks at display time only A policy change must not rewrite last year. Files: `src/totals/policy.ts` 3. Test both clock changes in your own zone It is two dates a year and it is always wrong first. Files: `tests/dst-totals.test.ts` ### Done when - [ ] An overnight shift counts on its start day - [ ] Totals are correct across both clock changes - [ ] A policy change alters no stored time - [ ] Approved months record their policy version ### 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. ## M3 · 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. ### Steps 1. Write audit rows in the data layer System actions are the ones people ask about. Files: `src/db/audited.ts`, `tests/system-audit.test.ts` 2. Lock on approval, unlock with a reason An unlock with no reason is the same as no lock. Files: `src/approvals/lock.ts` 3. Record before and after on every change One value tells you nothing in a dispute. Files: `src/audit/write.ts` ### Done when - [ ] A locked month refuses new versions - [ ] Unlocks carry an actor and a reason - [ ] Scheduled job changes are audited - [ ] Audit rows carry before and after values ### 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. ## M4 · 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. ### Steps 1. Show version history inline, with reasons The change is the part the worker needs to see. Files: `src/app/me/page.tsx` 2. Make a correction a request, never an edit Self-service editing defeats the whole record. Files: `src/requests/correction.ts` 3. Test cross-user access directly, not through the menu The menu is not the security boundary. Files: `tests/cross-user-access.test.ts` ### Done when - [ ] Workers see every version with actor and reason - [ ] Cross-user record access is refused - [ ] Correction requests modify no entries - [ ] Personal and administrator balances agree ### 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. ## M5 · Retention, export and a restore you have actually run (production only) 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. ### Steps 1. End-date leavers, never delete them The records outlive the employment by years. Files: `src/users/offboard.ts`, `tests/leaver-retention.test.ts` 2. Put the retention rule in the database A comment in the code is not a policy anybody can find. Files: `src/db/migrations/010_retention.sql` 3. Run the restore drill and write down the date The backup you have not restored is a directory. Files: `docs/restore-drill.md` ### Done when - [ ] Leavers keep entries, versions and audit rows - [ ] The retention rule and its check date are shown - [ ] Exports and the printed sheet agree with the screen - [ ] A restore reproduces totals and audit counts ### Watch 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. ===== OPERATIONS.md ===== # Operations · TimeTac ## Backup Nightly, offsite, retained longer than the record retention period. This is the one build in the catalogue where the backup policy is a legal obligation rather than an engineering preference. ## Restore Restore into an empty instance and compare monthly totals and audit row counts against the export taken before the restore. A restore that loses audit rows loses the part that settles a dispute. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Open entries older than a day, months approved without a policy version, the age of the last successful restore drill, and the age of the retention rule check. The last two are the ones that go stale without anything failing. ## Incident checklist The incident is a disputed hour, months later, and it is resolved by the version history and the audit trail or it is not resolved at all. Everything in this build that looks like extra work exists for that conversation. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No query is confirmed to update or delete a time value - [ ] An overnight shift is confirmed to count on its start day - [ ] A scheduled job change is confirmed to produce an audit row - [ ] A worker is confirmed able to read every version of their own entries - [ ] A leaver is confirmed to keep entries, versions and audit rows ## Launch constraint Do not market omitted TimeTac capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Entries, versions, absences, approvals and audit. Back this up nightly. DATABASE_PATH=./data/hours.db # Required · secret. Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. SESSION_SECRET=hex-64-chars-redacted # Required. The zone business days and clock changes are computed in. ORG_TIMEZONE=Europe/Madrid # Required. Set from the rule that applies to you, not from this example. RETENTION_YEARS=4 # Required. The rule you followed, stored beside the period and the date you checked it. RETENTION_RULE_REF=ET art 34.9, RDL 8/2019 # Required. Applied at display time only. Changing it must not alter a stored time. BREAK_DEDUCTION_MINUTES=30 # Required. Above this the deduction applies. Also display time only. BREAK_THRESHOLD_HOURS=6
# TimeTac · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS | people clock in from a phone browser and the record has to be reachable | | Storage | SQLite through a migration tool, one file, with no destructive update path | one file is easy to back up and the absence of hard deletes is enforced in the schema | | History | Append-only entry revisions plus an audit table, never an in-place edit | a corrected timesheet has to show what it said before | | Auth | Signed session cookies with two roles, no third-party identity provider | a time record that depends on somebody else's login is a time record you can lose access to | | Export | CSV and a printable monthly sheet per worker, generated from revisions | the exports are what somebody outside your company will actually read | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the scheduled jobs. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A machine your team can reach over HTTPS** · a 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 - [ ] **The working time recording rule that applies to you** · free 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 - [ ] **Backup storage away from the machine** · pennies 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 - [ ] **What you tell staff before switching it on** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for TimeTac, and the README should say so rather than pretend.
# Build brief · TimeTac The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a team time record that employees can read, that nobody can quietly edit, and that exports in a shape payroll accepts to replace TimeTac. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero These records are evidence, and that decides the architecture rather than decorating it. In Case C-55/18, decided on 14 May 2019, the Court of Justice of the European Union held that Member States must require employers to set up an objective, reliable and accessible system enabling the duration of time worked each day by each worker to be measured. Spain implemented that in Royal Decree-Law 8/2019: article 34.9 of the Estatuto de los Trabajadores requires a daily record including the specific start and finish time of each worker's day, kept for four years and available to the workers, their legal representatives and the labour inspectorate. Other countries have their own rules and their own retention periods. Find the one that applies to you before choosing a retention setting, and write down which rule you followed and when you checked it. Three consequences you cannot retrofit. Nothing is ever hard deleted; a correction is a new fact with the old one still readable. Every change carries who made it, when, and both values. And the person whose hours these are can read their own record without asking an administrator, because accessible is in the ruling and because a record only management can see is not evidence in the worker's favour. Second rule: this is a record of working time, not a monitoring system. No location, no screenshots, no idle detection, no keystroke counting. Those turn a record people are entitled to into surveillance that in many jurisdictions requires notice, a lawful basis and consultation with staff representatives before you switch it on. If somebody asks for that feature, the answer is a written policy, not a field. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS · people clock in from a phone browser and the record has to be reachable - Storage: SQLite through a migration tool, one file, with no destructive update path · one file is easy to back up and the absence of hard deletes is enforced in the schema - History: Append-only entry revisions plus an audit table, never an in-place edit · a corrected timesheet has to show what it said before - Auth: Signed session cookies with two roles, no third-party identity provider · a time record that depends on somebody else's login is a time record you can lose access to - Export: CSV and a printable monthly sheet per worker, generated from revisions · the exports are what somebody outside your company will actually read ### Data model (create this before Phase 1) - 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. ### Phase 1 · An entry that cannot vanish Build: A clock in and clock out control showing today's running total, and a week list. A correction inserts a new entry version and a new entry row superseding the old one, with a reason. There is no update statement against a time value and no delete statement against an entry anywhere in the codebase. Reject a second open entry for the same person. Trap: 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. Done when: No SQL in the codebase updates a time value or deletes an entry, asserted by a test that scans the migrations and query layer. Correcting an entry creates a new version with a reason and leaves the previous version readable, tested. A second clock-in while an entry is open is refused with a message naming the open entry. A voided entry is excluded from totals and still visible in the week list, tested. Do not build yet: No day arithmetic, no approvals, no absences, no exports. ### Phase 2 · The day's arithmetic, including the awkward days Build: Compute daily and weekly totals from entry versions. Store the business day on the entry rather than deriving it, so an overnight shift counts on the day it began. Apply a configurable unpaid break deduction at display time from the stored raw times, never by writing the deducted value. Test the arithmetic across a clock change in your own zone. Trap: 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. Done when: A shift starting at 22:00 and ending at 06:00 counts eight hours on the day it started, tested. Totals across a spring-forward and an autumn-back date in the configured zone are correct, tested with fixture dates for both. Changing the break policy does not alter any stored time value, verified in the database before and after. An approved month records the policy version it was approved under. Do not build yet: No approvals, no absences, no self-service, no exports. ### Phase 3 · Approval, locking and an audit that names names Build: An administrator view of a month per person with one-click approval that locks the month. A locked month refuses new entry versions unless an administrator explicitly unlocks it, and the unlock is itself an audit row with a reason. Every create, correct, void, approve and unlock writes an audit row with the actor, both values and the reason. Trap: 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. Done when: A locked month refuses a new entry version and the refusal names the lock, tested. An unlock writes an audit row with an actor and a reason and is visible in the month view, tested. A change made by a scheduled job produces an audit row with a system actor, tested by running the job. Audit rows carry before and after values for every field that changed, checked against a correction. Do not build yet: No self-service access, no absences, no exports. ### Phase 4 · The worker's own copy Build: A personal view of any period showing entries, versions, corrections with reasons and who made them, plus running balances against contracted hours and holiday. A correction request is a record the worker creates and an administrator acts on, never a direct edit. Absence requests follow the same shape: requested, decided, both recorded. Trap: 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. Done when: A worker can view every version of their own entries with the actor and the reason for each change, tested through the interface. A worker cannot read another worker's record through any route, tested by requesting one directly. A correction request creates a record and never modifies an entry, asserted in the suite. Balances are computed from versions and match the administrator's view for the same period, tested. Do not build yet: No exports, no retention handling, no backup drill. ### Phase 5 · Retention, export and a restore you have actually run Build: Store the retention rule reference, the period and the date you checked it in the database, and show them in the interface. Export entries, versions, absences, approvals and audit rows as CSV, plus a printable monthly sheet per person with signature lines. Nightly backups with a restore drill you run and document. Deleting a leaver removes contact details and keeps the time record. Trap: 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. Done when: Ending a user's employment keeps their entries, versions and audit rows and they still appear in exports for the retention period, tested. The retention rule reference, period and check date are stored and displayed, and the application warns when the check date is older than a year. Export produces every table as CSV and the monthly sheet matches the on-screen totals for the same period, tested. A restore from the nightly backup into an empty instance reproduces totals and audit counts, documented with the date you ran it. Do not build yet: Nothing after this. ### Out of scope (and why) - Location, geofencing, screenshots, idle detection and keystroke counting, which change this from a record into monitoring and carry notice, lawful basis and staff consultation obligations in many places. - Payroll and accounting integrations, because the value there is that your bookkeeper's system already accepts a specific file, and matching it is a per-customer contract rather than a feature. - Native mobile applications with offline clock-in, which is a genuine reason field teams pay, and a browser page on a phone is not the same thing. - Country-specific overtime, break and absence accrual rule sets, which are the actual product being sold and cannot be guessed at from the outside. ### README must contain - Which working time rule you are following, its retention period, and the date you checked it - That nothing is ever hard deleted, that corrections are new versions, and where the audit trail lives - That every worker can read their own full record including corrections, and how they request a change - That this is a record and not a monitoring tool, and the specific features refused on that basis - The backup schedule, the restore drill and the date it was last run successfully
# Agent instructions · TimeTac indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS, SQLite through a migration tool, one file, with no destructive update path, Append-only entry revisions plus an audit table, never an in-place edit, Signed session cookies with two roles, no third-party identity provider, CSV and a printable monthly sheet per worker, generated from revisions. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · TimeTac 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · An entry that cannot vanish Clocking in and out records a time entry that can be corrected but never removed or overwritten. ### Steps 1. Forbid update and delete on time values The test that scans for them is the one that keeps the rule. Files: `src/db/queries.ts`, `tests/no-destructive-sql.test.ts` 2. Make a correction a new version with a reason The reason is what an inspector or an employee will read. Files: `src/entries/correct.ts` 3. Refuse a second open entry Two open entries is an hours total nobody can defend. Files: `src/entries/clock.ts` ### Done when - [ ] No update or delete touches a time value - [ ] Corrections create versions with reasons - [ ] A second open entry is refused - [ ] Voided entries leave totals but stay visible ### 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. ## Phase 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. ### Steps 1. Store the business day, do not derive it An overnight shift belongs to the day it started. Files: `src/entries/day.ts`, `tests/overnight.test.ts` 2. Deduct breaks at display time only A policy change must not rewrite last year. Files: `src/totals/policy.ts` 3. Test both clock changes in your own zone It is two dates a year and it is always wrong first. Files: `tests/dst-totals.test.ts` ### Done when - [ ] An overnight shift counts on its start day - [ ] Totals are correct across both clock changes - [ ] A policy change alters no stored time - [ ] Approved months record their policy version ### 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. ## Phase 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. ### Steps 1. Write audit rows in the data layer System actions are the ones people ask about. Files: `src/db/audited.ts`, `tests/system-audit.test.ts` 2. Lock on approval, unlock with a reason An unlock with no reason is the same as no lock. Files: `src/approvals/lock.ts` 3. Record before and after on every change One value tells you nothing in a dispute. Files: `src/audit/write.ts` ### Done when - [ ] A locked month refuses new versions - [ ] Unlocks carry an actor and a reason - [ ] Scheduled job changes are audited - [ ] Audit rows carry before and after values ### 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. ## Phase 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. ### Steps 1. Show version history inline, with reasons The change is the part the worker needs to see. Files: `src/app/me/page.tsx` 2. Make a correction a request, never an edit Self-service editing defeats the whole record. Files: `src/requests/correction.ts` 3. Test cross-user access directly, not through the menu The menu is not the security boundary. Files: `tests/cross-user-access.test.ts` ### Done when - [ ] Workers see every version with actor and reason - [ ] Cross-user record access is refused - [ ] Correction requests modify no entries - [ ] Personal and administrator balances agree ### 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. ## Not in this build - Location, geofencing, screenshots and idle detection. - Payroll and accounting integrations. - Native mobile applications with offline clock-in. - Country-specific overtime and accrual rule sets. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Entries, versions, absences, approvals and audit. Back this up nightly. DATABASE_PATH=./data/hours.db # Required · secret. Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. SESSION_SECRET=hex-64-chars-redacted # Required. The zone business days and clock changes are computed in. ORG_TIMEZONE=Europe/Madrid # Required. Set from the rule that applies to you, not from this example. RETENTION_YEARS=4 # Required. The rule you followed, stored beside the period and the date you checked it. RETENTION_RULE_REF=ET art 34.9, RDL 8/2019 # Required. Applied at display time only. Changing it must not alter a stored time. BREAK_DEDUCTION_MINUTES=30 # Required. Above this the deduction applies. Also display time only. BREAK_THRESHOLD_HOURS=6
# TimeTac · product brief ## Problem If you are one person logging hours against projects, this is a solved evening: a table, a timer, a CSV export, done. TimeTac is not sold for that, it is sold to companies that must prove who worked when, which in Spain means a daily time record that survives an inspection. An agent can absolutely build the clock-in screen, the leave calendar and the monthly timesheet PDF, and it will feel surprisingly complete. What it will not hand you is the tamper-resistant audit trail, the payroll and HR exports your accountant already accepts, and the phone app your field staff will actually open. So: yes for yourself, shaky the moment other people's paychecks depend on your SQLite file. ## Product outcome A working time record where corrections are visible rather than silent, every worker can read their own history, a month can be locked and only deliberately unlocked, and four years of evidence survive a departing employee and a dead disk. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20+ - somewhere to run it that your team can reach, plus HTTPS if it leaves your laptop - backups you actually test, because these records are the evidence ## Explicit non-goals for v1 - 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 ## Success criteria - No query is confirmed to update or delete a time value - An overnight shift is confirmed to count on its start day - A scheduled job change is confirmed to produce an audit row - A worker is confirmed able to read every version of their own entries - A leaver is confirmed to keep entries, versions and audit rows
# Build brief · TimeTac The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a team time record that employees can read, that nobody can quietly edit, and that exports in a shape payroll accepts to replace TimeTac. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero These records are evidence, and that decides the architecture rather than decorating it. In Case C-55/18, decided on 14 May 2019, the Court of Justice of the European Union held that Member States must require employers to set up an objective, reliable and accessible system enabling the duration of time worked each day by each worker to be measured. Spain implemented that in Royal Decree-Law 8/2019: article 34.9 of the Estatuto de los Trabajadores requires a daily record including the specific start and finish time of each worker's day, kept for four years and available to the workers, their legal representatives and the labour inspectorate. Other countries have their own rules and their own retention periods. Find the one that applies to you before choosing a retention setting, and write down which rule you followed and when you checked it. Three consequences you cannot retrofit. Nothing is ever hard deleted; a correction is a new fact with the old one still readable. Every change carries who made it, when, and both values. And the person whose hours these are can read their own record without asking an administrator, because accessible is in the ruling and because a record only management can see is not evidence in the worker's favour. Second rule: this is a record of working time, not a monitoring system. No location, no screenshots, no idle detection, no keystroke counting. Those turn a record people are entitled to into surveillance that in many jurisdictions requires notice, a lawful basis and consultation with staff representatives before you switch it on. If somebody asks for that feature, the answer is a written policy, not a field. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS · people clock in from a phone browser and the record has to be reachable - Storage: SQLite through a migration tool, one file, with no destructive update path · one file is easy to back up and the absence of hard deletes is enforced in the schema - History: Append-only entry revisions plus an audit table, never an in-place edit · a corrected timesheet has to show what it said before - Auth: Signed session cookies with two roles, no third-party identity provider · a time record that depends on somebody else's login is a time record you can lose access to - Export: CSV and a printable monthly sheet per worker, generated from revisions · the exports are what somebody outside your company will actually read ### Data model (create this before Phase 1) - 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. ### Phase 1 · An entry that cannot vanish Build: A clock in and clock out control showing today's running total, and a week list. A correction inserts a new entry version and a new entry row superseding the old one, with a reason. There is no update statement against a time value and no delete statement against an entry anywhere in the codebase. Reject a second open entry for the same person. Trap: 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. Done when: No SQL in the codebase updates a time value or deletes an entry, asserted by a test that scans the migrations and query layer. Correcting an entry creates a new version with a reason and leaves the previous version readable, tested. A second clock-in while an entry is open is refused with a message naming the open entry. A voided entry is excluded from totals and still visible in the week list, tested. Do not build yet: No day arithmetic, no approvals, no absences, no exports. ### Phase 2 · The day's arithmetic, including the awkward days Build: Compute daily and weekly totals from entry versions. Store the business day on the entry rather than deriving it, so an overnight shift counts on the day it began. Apply a configurable unpaid break deduction at display time from the stored raw times, never by writing the deducted value. Test the arithmetic across a clock change in your own zone. Trap: 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. Done when: A shift starting at 22:00 and ending at 06:00 counts eight hours on the day it started, tested. Totals across a spring-forward and an autumn-back date in the configured zone are correct, tested with fixture dates for both. Changing the break policy does not alter any stored time value, verified in the database before and after. An approved month records the policy version it was approved under. Do not build yet: No approvals, no absences, no self-service, no exports. ### Phase 3 · Approval, locking and an audit that names names Build: An administrator view of a month per person with one-click approval that locks the month. A locked month refuses new entry versions unless an administrator explicitly unlocks it, and the unlock is itself an audit row with a reason. Every create, correct, void, approve and unlock writes an audit row with the actor, both values and the reason. Trap: 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. Done when: A locked month refuses a new entry version and the refusal names the lock, tested. An unlock writes an audit row with an actor and a reason and is visible in the month view, tested. A change made by a scheduled job produces an audit row with a system actor, tested by running the job. Audit rows carry before and after values for every field that changed, checked against a correction. Do not build yet: No self-service access, no absences, no exports. ### Phase 4 · The worker's own copy Build: A personal view of any period showing entries, versions, corrections with reasons and who made them, plus running balances against contracted hours and holiday. A correction request is a record the worker creates and an administrator acts on, never a direct edit. Absence requests follow the same shape: requested, decided, both recorded. Trap: 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. Done when: A worker can view every version of their own entries with the actor and the reason for each change, tested through the interface. A worker cannot read another worker's record through any route, tested by requesting one directly. A correction request creates a record and never modifies an entry, asserted in the suite. Balances are computed from versions and match the administrator's view for the same period, tested. Do not build yet: No exports, no retention handling, no backup drill. ### Phase 5 · Retention, export and a restore you have actually run Build: Store the retention rule reference, the period and the date you checked it in the database, and show them in the interface. Export entries, versions, absences, approvals and audit rows as CSV, plus a printable monthly sheet per person with signature lines. Nightly backups with a restore drill you run and document. Deleting a leaver removes contact details and keeps the time record. Trap: 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. Done when: Ending a user's employment keeps their entries, versions and audit rows and they still appear in exports for the retention period, tested. The retention rule reference, period and check date are stored and displayed, and the application warns when the check date is older than a year. Export produces every table as CSV and the monthly sheet matches the on-screen totals for the same period, tested. A restore from the nightly backup into an empty instance reproduces totals and audit counts, documented with the date you ran it. Do not build yet: Nothing after this. ### Out of scope (and why) - Location, geofencing, screenshots, idle detection and keystroke counting, which change this from a record into monitoring and carry notice, lawful basis and staff consultation obligations in many places. - Payroll and accounting integrations, because the value there is that your bookkeeper's system already accepts a specific file, and matching it is a per-customer contract rather than a feature. - Native mobile applications with offline clock-in, which is a genuine reason field teams pay, and a browser page on a phone is not the same thing. - Country-specific overtime, break and absence accrual rule sets, which are the actual product being sold and cannot be guessed at from the outside. ### README must contain - Which working time rule you are following, its retention period, and the date you checked it - That nothing is ever hard deleted, that corrections are new versions, and where the audit trail lives - That every worker can read their own full record including corrections, and how they request a change - That this is a record and not a monitoring tool, and the specific features refused on that basis - The backup schedule, the restore drill and the date it was last run successfully
# Architecture · TimeTac ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS | people clock in from a phone browser and the record has to be reachable | | Storage | SQLite through a migration tool, one file, with no destructive update path | one file is easy to back up and the absence of hard deletes is enforced in the schema | | History | Append-only entry revisions plus an audit table, never an in-place edit | a corrected timesheet has to show what it said before | | Auth | Signed session cookies with two roles, no third-party identity provider | a time record that depends on somebody else's login is a time record you can lose access to | | Export | CSV and a printable monthly sheet per worker, generated from revisions | the exports are what somebody outside your company will actually read | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Entries | Clocking, versions, voids and the no-delete rule. | Any store, provided no query updates or deletes a time value. | | Totals | Business days, overnight shifts and display-time break policy. | Any implementation, provided the policy is never written into stored times. | | Approvals | Month locking, unlocks with reasons and policy versions. | Any workflow, provided an unlock is audited as loudly as an approval. | | Audit | Data-layer attribution including system actions. | Not swappable for interface-layer logging, which misses the job at midnight. | | Retention | The rule reference, leaver handling and exports. | Any policy, provided leavers keep their records for the full period. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Entries, versions, absences, approvals and audit. Back this up nightly. - `SESSION_SECRET` · required, secret · Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. - `ORG_TIMEZONE` · required · The zone business days and clock changes are computed in. - `RETENTION_YEARS` · required · Set from the rule that applies to you, not from this example. - `RETENTION_RULE_REF` · required · The rule you followed, stored beside the period and the date you checked it. - `BREAK_DEDUCTION_MINUTES` · required · Applied at display time only. Changing it must not alter a stored time. - `BREAK_THRESHOLD_HOURS` · required · Above this the deduction applies. Also display time only. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · TimeTac product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, self-hosted on a machine your team can reach over HTTPS, SQLite through a migration tool, one file, with no destructive update path, Append-only entry revisions plus an audit table, never an in-place edit, Signed session cookies with two roles, no third-party identity provider, CSV and a printable monthly sheet per worker, generated from revisions. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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.
# Delivery milestones · TimeTac Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · An entry that cannot vanish Clocking in and out records a time entry that can be corrected but never removed or overwritten. ### Steps 1. Forbid update and delete on time values The test that scans for them is the one that keeps the rule. Files: `src/db/queries.ts`, `tests/no-destructive-sql.test.ts` 2. Make a correction a new version with a reason The reason is what an inspector or an employee will read. Files: `src/entries/correct.ts` 3. Refuse a second open entry Two open entries is an hours total nobody can defend. Files: `src/entries/clock.ts` ### Done when - [ ] No update or delete touches a time value - [ ] Corrections create versions with reasons - [ ] A second open entry is refused - [ ] Voided entries leave totals but stay visible ### 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. ## M2 · 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. ### Steps 1. Store the business day, do not derive it An overnight shift belongs to the day it started. Files: `src/entries/day.ts`, `tests/overnight.test.ts` 2. Deduct breaks at display time only A policy change must not rewrite last year. Files: `src/totals/policy.ts` 3. Test both clock changes in your own zone It is two dates a year and it is always wrong first. Files: `tests/dst-totals.test.ts` ### Done when - [ ] An overnight shift counts on its start day - [ ] Totals are correct across both clock changes - [ ] A policy change alters no stored time - [ ] Approved months record their policy version ### 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. ## M3 · 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. ### Steps 1. Write audit rows in the data layer System actions are the ones people ask about. Files: `src/db/audited.ts`, `tests/system-audit.test.ts` 2. Lock on approval, unlock with a reason An unlock with no reason is the same as no lock. Files: `src/approvals/lock.ts` 3. Record before and after on every change One value tells you nothing in a dispute. Files: `src/audit/write.ts` ### Done when - [ ] A locked month refuses new versions - [ ] Unlocks carry an actor and a reason - [ ] Scheduled job changes are audited - [ ] Audit rows carry before and after values ### 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. ## M4 · 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. ### Steps 1. Show version history inline, with reasons The change is the part the worker needs to see. Files: `src/app/me/page.tsx` 2. Make a correction a request, never an edit Self-service editing defeats the whole record. Files: `src/requests/correction.ts` 3. Test cross-user access directly, not through the menu The menu is not the security boundary. Files: `tests/cross-user-access.test.ts` ### Done when - [ ] Workers see every version with actor and reason - [ ] Cross-user record access is refused - [ ] Correction requests modify no entries - [ ] Personal and administrator balances agree ### 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. ## M5 · Retention, export and a restore you have actually run (production only) 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. ### Steps 1. End-date leavers, never delete them The records outlive the employment by years. Files: `src/users/offboard.ts`, `tests/leaver-retention.test.ts` 2. Put the retention rule in the database A comment in the code is not a policy anybody can find. Files: `src/db/migrations/010_retention.sql` 3. Run the restore drill and write down the date The backup you have not restored is a directory. Files: `docs/restore-drill.md` ### Done when - [ ] Leavers keep entries, versions and audit rows - [ ] The retention rule and its check date are shown - [ ] Exports and the printed sheet agree with the screen - [ ] A restore reproduces totals and audit counts ### Watch 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.
# Operations · TimeTac ## Backup Nightly, offsite, retained longer than the record retention period. This is the one build in the catalogue where the backup policy is a legal obligation rather than an engineering preference. ## Restore Restore into an empty instance and compare monthly totals and audit row counts against the export taken before the restore. A restore that loses audit rows loses the part that settles a dispute. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Open entries older than a day, months approved without a policy version, the age of the last successful restore drill, and the age of the retention rule check. The last two are the ones that go stale without anything failing. ## Incident checklist The incident is a disputed hour, months later, and it is resolved by the version history and the audit trail or it is not resolved at all. Everything in this build that looks like extra work exists for that conversation. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No query is confirmed to update or delete a time value - [ ] An overnight shift is confirmed to count on its start day - [ ] A scheduled job change is confirmed to produce an audit row - [ ] A worker is confirmed able to read every version of their own entries - [ ] A leaver is confirmed to keep entries, versions and audit rows ## Launch constraint Do not market omitted TimeTac capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Entries, versions, absences, approvals and audit. Back this up nightly. DATABASE_PATH=./data/hours.db # Required · secret. Signs session cookies. Rotating it logs everybody out, which is the intended behaviour. SESSION_SECRET=hex-64-chars-redacted # Required. The zone business days and clock changes are computed in. ORG_TIMEZONE=Europe/Madrid # Required. Set from the rule that applies to you, not from this example. RETENTION_YEARS=4 # Required. The rule you followed, stored beside the period and the date you checked it. RETENTION_RULE_REF=ET art 34.9, RDL 8/2019 # Required. Applied at display time only. Changing it must not alter a stored time. BREAK_DEDUCTION_MINUTES=30 # Required. Above this the deduction applies. Also display time only. BREAK_THRESHOLD_HOURS=6
$ choose a build depth, inspect the files, then open the complete pack in your agent
Companies do not buy time tracking because timers are hard, they buy it because a missing time record is a fine and a disputed overtime claim is a lawsuit. The paid version comes with rule sets for breaks, overtime and absence accrual per country, exports that payroll accepts, apps that work on a phone in a van, and a vendor to point at when something is wrong. A self-hosted app you wrote yourself is fine for your own invoicing and a bad idea the moment an employee disagrees with it.
xLegally defensible records: no tamper-evident log, no retention guarantees, nothing an inspector or works council will nod at
xNative iOS and Android apps with GPS, offline clock-in and geofencing for people who are not at a desk
xHardware terminals and badge or fingerprint clock-in for shop floors
xPayroll, ERP and HR integrations plus the export formats your bookkeeper already imports without complaining
xSomeone else owning uptime, overtime rule edge cases and the shift and break math for each country
Nothing worth pointing at. That's why the prompt exists.
Vibecode TimeTac
Kinda. The core of TimeTac is buildable in a weekend with the prompt on this page, but there are real gaps: 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. Read the honest list above before committing.
How much does TimeTac cost?
TimeTac costs about $7.2/month (Employee Time Tracking module, checked 2026-08-18), which is $86.4 per year.
What do I lose by replacing TimeTac?
Honestly: 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. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to TimeTac?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.