Vibecode When I Work
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For When I Work, build a small shift schedule, availability form, and swap approval workflow. The hard boundary is mobile apps, time clock, notifications, scheduling edge cases, and support, plus compliance, payroll, and regulated integrations.
You are building a lean indie version of When I Work. 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 ===== # When I Work · indie build A schedule for one team: shifts on a rota, availability submitted by staff, swap and cover requests that require a named approval, a change history nobody can quietly edit, and an export that a payroll system or an accountant can consume without this application interpreting 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 | Node 22 with PostgreSQL | shift data is small, relational and consequential, which is what a real database is for | | Deployment | Self-hosted and private, not publicly indexed | this holds staff names, availability and absence patterns | | Time | A library with a full time zone database, storing instants plus the local zone | a shift is a local wall clock time and a clock change must not move it | | Audit | Append-only change records, never updated or deleted | an approval trail that can be rewritten is not a trail | | Export | CSV of hours worked, with no monetary calculation anywhere | payroll is somebody else's regulated responsibility and should stay there | ## 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. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Holds staff records and an append-only change log, both of which want a real database. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **An understanding of what you must not automate** · free - Why: Scheduling software sits next to employment law, and the boundary between recording a decision and making one is the whole design. Crossing it changes your responsibility. - Get it: Write down, before building, that this application will not calculate pay, will not assert compliance and will not decide anything. If your situation needs those, buy a product whose vendor carries that responsibility. - Verify: The boundary statement is written and appears in the README - [ ] **A retention policy for staff data** · free - Why: This holds personal data about identifiable people, including absence patterns. How long you keep it is a decision with legal weight in many places. - Get it: Decide the retention period for each kind of record and write the reason. Employment records often have a minimum retention that differs from the maximum you would otherwise want. - Verify: The policy is written with a reason per record kind - [ ] **A real rota for a real week** · free - Why: Overnight shifts, split shifts and someone who only works Tuesdays are all present in real rotas and absent from invented ones, and each breaks a naive model. - Get it: Use an actual week from the team this is for, including its awkward shifts. - Verify: A real week is entered including at least one overnight or split shift ## 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: - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. - mobile apps, time clock, notifications, scheduling edge cases, and support - payroll rails - tax and labor-law compliance - benefits integrations - identity checks and global employment infrastructure If one of those is essential to you, that is the reason to keep paying for When I Work, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · When I Work 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 small shift schedule with availability, swaps and an approval trail, for one team to replace When I Work. 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 This schedules shifts and records approvals. It does not calculate pay, it does not check working time law, and it does not make employment decisions. Those three boundaries are not modesty; crossing any of them means your code is participating in a regulated decision about someone's livelihood, with the liability that implies. So it exports data for a payroll system rather than computing money, it warns about patterns rather than asserting compliance, and every consequential action carries a named human approver. Build it so the record shows who decided, not what the software decided. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · shift data is small, relational and consequential, which is what a real database is for - Deployment: Self-hosted and private, not publicly indexed · this holds staff names, availability and absence patterns - Time: A library with a full time zone database, storing instants plus the local zone · a shift is a local wall clock time and a clock change must not move it - Audit: Append-only change records, never updated or deleted · an approval trail that can be rewritten is not a trail - Export: CSV of hours worked, with no monetary calculation anywhere · payroll is somebody else's regulated responsibility and should stay there ### Data model (create this before Phase 1) - employees(id, name, email, role, employment_kind, active_from, active_to) · employment_kind is recorded, never interpreted - positions(id, name, colour), locations(id, name, timezone) - shifts(id, location_id, position_id, employee_id, starts_at_utc, ends_at_utc, local_date, state, published_at) - availability(id, employee_id, weekday, start_local, end_local, kind, effective_from) · kind is available, preferred or unavailable - time_off(id, employee_id, starts_on, ends_on, kind, state, decided_by, decided_at, note) - swaps(id, shift_id, from_employee_id, to_employee_id, state, requested_at, decided_by, decided_at) - changes(id, entity, entity_id, at, actor_id, before_json, after_json) · append only, no update or delete path - Every state change on a shift, swap or absence records who decided it. There is no automatic approval anywhere. ### Phase 1 · Shifts on a rota, with time handled properly Build: Create shifts against a location with its own time zone. Store the instant and the local date. A week view showing the rota. Publishing is an explicit action that marks shifts visible to staff. Trap: A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. Done when: A week copied across a daylight saving transition keeps its shifts at the same local times. Unpublished shifts are invisible to staff accounts, verified by requesting them directly. The local date on a shift matches the day staff would call it, including for an overnight shift. Publishing is recorded with who published and when. Do not build yet: No availability, swaps or exports. ### Phase 2 · Availability, submitted by the people it describes Build: Availability submitted by each employee with an effective date, so a change applies from when it was agreed rather than retroactively. When a shift is assigned outside someone's stated availability, warn the scheduler rather than blocking, and record that the warning was seen. Trap: Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. Done when: An employee submits availability and it applies from its effective date without altering past assignments. Assigning a shift outside availability produces a warning that must be acknowledged. The acknowledgement is recorded with who acknowledged it. A past week's warnings are visible in the history. Do not build yet: No swaps or absence yet. ### Phase 3 · Swaps and cover, with a named approver Build: A swap request naming the shift and the proposed cover. The receiving employee accepts, then a manager approves. Nothing changes the published rota until both have happened, and every step records who did it. Trap: Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. Done when: A swap requires acceptance and then approval before the rota changes. The rota is unchanged while a swap is pending. Each step records the individual who performed it. A rejected swap leaves the original assignment and records the rejection with a reason. Do not build yet: No absence or export yet. ### Phase 4 · Absence, recorded rather than adjudicated Build: Absence requests with a kind and dates. A manager decides, and the decision is recorded with who and when and any note. Approved absence blocks assignment with a warning. Record entitlement balances if you track them, but do not calculate eligibility. Trap: Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. Done when: An absence request is decided by a named person and the decision is recorded. Assigning a shift during approved absence warns and requires acknowledgement. Entitlement figures, if present, are recorded values that the application never computes, verified by inspecting the code for any accrual arithmetic. A decision cannot be edited, only superseded by a new one. Do not build yet: No export yet. ### Phase 5 · Export, where the money is not calculated Build: Export a period as CSV: employee, date, shift start and end, hours, position and any recorded notes. No rates, no totals in money, no overtime classification. Include the approval trail alongside so a query can be answered. Trap: Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. Done when: An export contains hours and dates with no monetary value and no overtime classification anywhere. The export matches the published rota for the period, verified against a hand count for one employee. The approval trail for the period exports alongside. Re-exporting an unchanged period produces identical output. Do not build yet: Operations and retention come last. ### Phase 6 · Access, retention and the records you must keep Build: Roles limiting staff to their own data and managers to their team. A written retention policy applied by a job. Backups. A subject access export producing everything held about one person. And a written statement of what this application does not do. Trap: Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. Done when: An employee can see their own shifts and not a colleague's, tested by requesting directly. A subject access export produces everything held about one person in a readable form. Retention removes data past the policy while keeping what employment records require. The README states plainly that this calculates no pay and makes no employment decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Payroll calculation of any kind. Hours out, money elsewhere, deliberately. - Working time and labour law compliance checking, which varies by jurisdiction and changes, and being wrong about it affects someone's rights. - Employment decisions. This records what people decided; it never decides. - A time clock with attendance verification, which is a hardware and trust problem of its own. - Mobile applications with push notifications, which is most of how staff actually use this category. ### README must contain - That this calculates no pay, checks no compliance and makes no employment decisions - That shifts are local wall clock commitments and survive clock changes - That availability and absence produce warnings requiring acknowledgement, never blocks - That every state change records a named individual, and the change log is append only - The retention policy, and how a subject access request is answered ===== AGENTS.md ===== # Agent instructions · When I Work indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with PostgreSQL, Self-hosted and private, not publicly indexed, A library with a full time zone database, storing instants plus the local zone, Append-only change records, never updated or deleted, CSV of hours worked, with no monetary calculation anywhere. 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 - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ===== BUILD_PLAN.md ===== # Build plan · When I Work A schedule for one team: shifts on a rota, availability submitted by staff, swap and cover requests that require a named approval, a change history nobody can quietly edit, and an export that a payroll system or an accountant can consume without this application interpreting 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 · Shifts on a rota, with time handled properly A published week shows the right shifts at the right local times. ### Steps 1. Model shifts with location zones Instant plus the local date. The zone belongs to the location, not the server. Files: `src/shifts.js` 2. Handle overnight shifts A shift crossing midnight belongs to the day it started, which is what staff and payroll both expect. Decide and document it. Files: `src/shifts.js` 3. Make publishing explicit A draft rota is not a commitment. Publishing is an action with an actor and a timestamp. Files: `src/publish.js` ### Done when - [ ] A week copied across a clock change keeps local times - [ ] Unpublished shifts are refused to staff accounts directly - [ ] An overnight shift's local date matches what staff would say - [ ] Publishing records who and when ### Watch out - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. ## Phase 2 · Availability, submitted by the people it describes Staff say when they can work, and the schedule warns when it contradicts them. ### Steps 1. Let staff submit their own availability It describes them, so they enter it. A manager editing it silently is the pattern to avoid. Files: `src/availability.js` 2. Apply from an effective date A change agreed in March applies from March, not to January's rota. Files: `src/availability.js` 3. Warn and record acknowledgement Never block. Record who acknowledged, because the record is the point. Files: `src/warnings.js` ### Done when - [ ] Availability applies from its effective date - [ ] An out-of-availability assignment warns and requires acknowledgement - [ ] The acknowledgement records who and when - [ ] Past warnings remain visible in history ### Watch out - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. ## Phase 3 · Swaps and cover, with a named approver Staff can arrange cover between themselves, and a manager approves it explicitly. ### Steps 1. Model the two-step flow Request, accept, approve. Each with an actor and a timestamp. Files: `src/swaps.js` 2. Leave the rota alone until approved A pending swap is not a schedule change. Verify the published rota is unaffected. Files: `src/swaps.js` 3. Record rejections with reasons A refused swap is as important a record as an approved one. Files: `src/swaps.js` ### Done when - [ ] A swap changes the rota only after acceptance and approval - [ ] A pending swap leaves the published rota unchanged - [ ] Every step records a named individual - [ ] A rejection is recorded with a reason ### Watch out - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. ## Phase 4 · Absence, recorded rather than adjudicated Time off is requested, decided by a person, and the schedule reflects it. ### Steps 1. Model requests and decisions Separate rows. A decision references a request rather than mutating it. Files: `src/timeoff.js` 2. Warn on conflicting assignment Approved absence is a strong warning, still not a block, and still acknowledged. Files: `src/warnings.js` 3. Record entitlement, never derive it A number a person entered from the contract. No accrual arithmetic anywhere. Files: `src/entitlement.js` ### Done when - [ ] An absence decision records a named person - [ ] A conflicting assignment warns and is acknowledged - [ ] No accrual or eligibility arithmetic exists in the code - [ ] A decision is superseded rather than edited ### Watch out - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. ## Phase 5 · Export, where the money is not calculated A payroll period exports as hours that somebody else turns into pay. ### Steps 1. Export hours and dates only No rates, no money, no classification. Grep the exporter for currency handling and find none. Files: `src/export.js` 2. Verify against a hand count One employee, one period, counted by hand. Arithmetic that nobody checked is arithmetic that is wrong. 3. Export the approval trail So a payroll query about a changed shift can be answered without opening the application. Files: `src/export.js` ### Done when - [ ] No monetary value or overtime classification appears in the export - [ ] The export matches a hand count for one employee - [ ] The approval trail exports alongside - [ ] Re-exporting an unchanged period is identical ### Watch out - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ## Not in this build - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. ## After v1, if you want it - Shift templates for a repeating weekly pattern, which saves most of the scheduling time - An open shift board staff can claim, still with an approval step - A coverage view showing gaps by position before publishing ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. This database holds personal data. DATABASE_URL=postgres://user:pass@localhost:5432/rota # Required. Time zone for a location. Shifts are local wall clock times in it. DEFAULT_LOCATION_TZ=Europe/London # Required. Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. OVERNIGHT_BELONGS_TO=start # Required. How long schedule records are kept, per your written policy. RETENTION_MONTHS_SCHEDULE=24 # Required. How long the change log is kept. Usually longer, because it is the record of decisions. RETENTION_MONTHS_AUDIT=72 # Required. Where the application is reachable. Private and not publicly indexed. PUBLIC_URL=https://rota.internal.example.com
You are building a lean indie version of When I Work. 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 ===== # When I Work · indie build A schedule for one team: shifts on a rota, availability submitted by staff, swap and cover requests that require a named approval, a change history nobody can quietly edit, and an export that a payroll system or an accountant can consume without this application interpreting 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 | Node 22 with PostgreSQL | shift data is small, relational and consequential, which is what a real database is for | | Deployment | Self-hosted and private, not publicly indexed | this holds staff names, availability and absence patterns | | Time | A library with a full time zone database, storing instants plus the local zone | a shift is a local wall clock time and a clock change must not move it | | Audit | Append-only change records, never updated or deleted | an approval trail that can be rewritten is not a trail | | Export | CSV of hours worked, with no monetary calculation anywhere | payroll is somebody else's regulated responsibility and should stay there | ## 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. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Holds staff records and an append-only change log, both of which want a real database. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **An understanding of what you must not automate** · free - Why: Scheduling software sits next to employment law, and the boundary between recording a decision and making one is the whole design. Crossing it changes your responsibility. - Get it: Write down, before building, that this application will not calculate pay, will not assert compliance and will not decide anything. If your situation needs those, buy a product whose vendor carries that responsibility. - Verify: The boundary statement is written and appears in the README - [ ] **A retention policy for staff data** · free - Why: This holds personal data about identifiable people, including absence patterns. How long you keep it is a decision with legal weight in many places. - Get it: Decide the retention period for each kind of record and write the reason. Employment records often have a minimum retention that differs from the maximum you would otherwise want. - Verify: The policy is written with a reason per record kind - [ ] **A real rota for a real week** · free - Why: Overnight shifts, split shifts and someone who only works Tuesdays are all present in real rotas and absent from invented ones, and each breaks a naive model. - Get it: Use an actual week from the team this is for, including its awkward shifts. - Verify: A real week is entered including at least one overnight or split shift ## 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: - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. - mobile apps, time clock, notifications, scheduling edge cases, and support - payroll rails - tax and labor-law compliance - benefits integrations - identity checks and global employment infrastructure If one of those is essential to you, that is the reason to keep paying for When I Work, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · When I Work 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 small shift schedule with availability, swaps and an approval trail, for one team to replace When I Work. 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 This schedules shifts and records approvals. It does not calculate pay, it does not check working time law, and it does not make employment decisions. Those three boundaries are not modesty; crossing any of them means your code is participating in a regulated decision about someone's livelihood, with the liability that implies. So it exports data for a payroll system rather than computing money, it warns about patterns rather than asserting compliance, and every consequential action carries a named human approver. Build it so the record shows who decided, not what the software decided. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · shift data is small, relational and consequential, which is what a real database is for - Deployment: Self-hosted and private, not publicly indexed · this holds staff names, availability and absence patterns - Time: A library with a full time zone database, storing instants plus the local zone · a shift is a local wall clock time and a clock change must not move it - Audit: Append-only change records, never updated or deleted · an approval trail that can be rewritten is not a trail - Export: CSV of hours worked, with no monetary calculation anywhere · payroll is somebody else's regulated responsibility and should stay there ### Data model (create this before Phase 1) - employees(id, name, email, role, employment_kind, active_from, active_to) · employment_kind is recorded, never interpreted - positions(id, name, colour), locations(id, name, timezone) - shifts(id, location_id, position_id, employee_id, starts_at_utc, ends_at_utc, local_date, state, published_at) - availability(id, employee_id, weekday, start_local, end_local, kind, effective_from) · kind is available, preferred or unavailable - time_off(id, employee_id, starts_on, ends_on, kind, state, decided_by, decided_at, note) - swaps(id, shift_id, from_employee_id, to_employee_id, state, requested_at, decided_by, decided_at) - changes(id, entity, entity_id, at, actor_id, before_json, after_json) · append only, no update or delete path - Every state change on a shift, swap or absence records who decided it. There is no automatic approval anywhere. ### Phase 1 · Shifts on a rota, with time handled properly Build: Create shifts against a location with its own time zone. Store the instant and the local date. A week view showing the rota. Publishing is an explicit action that marks shifts visible to staff. Trap: A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. Done when: A week copied across a daylight saving transition keeps its shifts at the same local times. Unpublished shifts are invisible to staff accounts, verified by requesting them directly. The local date on a shift matches the day staff would call it, including for an overnight shift. Publishing is recorded with who published and when. Do not build yet: No availability, swaps or exports. ### Phase 2 · Availability, submitted by the people it describes Build: Availability submitted by each employee with an effective date, so a change applies from when it was agreed rather than retroactively. When a shift is assigned outside someone's stated availability, warn the scheduler rather than blocking, and record that the warning was seen. Trap: Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. Done when: An employee submits availability and it applies from its effective date without altering past assignments. Assigning a shift outside availability produces a warning that must be acknowledged. The acknowledgement is recorded with who acknowledged it. A past week's warnings are visible in the history. Do not build yet: No swaps or absence yet. ### Phase 3 · Swaps and cover, with a named approver Build: A swap request naming the shift and the proposed cover. The receiving employee accepts, then a manager approves. Nothing changes the published rota until both have happened, and every step records who did it. Trap: Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. Done when: A swap requires acceptance and then approval before the rota changes. The rota is unchanged while a swap is pending. Each step records the individual who performed it. A rejected swap leaves the original assignment and records the rejection with a reason. Do not build yet: No absence or export yet. ### Phase 4 · Absence, recorded rather than adjudicated Build: Absence requests with a kind and dates. A manager decides, and the decision is recorded with who and when and any note. Approved absence blocks assignment with a warning. Record entitlement balances if you track them, but do not calculate eligibility. Trap: Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. Done when: An absence request is decided by a named person and the decision is recorded. Assigning a shift during approved absence warns and requires acknowledgement. Entitlement figures, if present, are recorded values that the application never computes, verified by inspecting the code for any accrual arithmetic. A decision cannot be edited, only superseded by a new one. Do not build yet: No export yet. ### Phase 5 · Export, where the money is not calculated Build: Export a period as CSV: employee, date, shift start and end, hours, position and any recorded notes. No rates, no totals in money, no overtime classification. Include the approval trail alongside so a query can be answered. Trap: Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. Done when: An export contains hours and dates with no monetary value and no overtime classification anywhere. The export matches the published rota for the period, verified against a hand count for one employee. The approval trail for the period exports alongside. Re-exporting an unchanged period produces identical output. Do not build yet: Operations and retention come last. ### Phase 6 · Access, retention and the records you must keep Build: Roles limiting staff to their own data and managers to their team. A written retention policy applied by a job. Backups. A subject access export producing everything held about one person. And a written statement of what this application does not do. Trap: Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. Done when: An employee can see their own shifts and not a colleague's, tested by requesting directly. A subject access export produces everything held about one person in a readable form. Retention removes data past the policy while keeping what employment records require. The README states plainly that this calculates no pay and makes no employment decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Payroll calculation of any kind. Hours out, money elsewhere, deliberately. - Working time and labour law compliance checking, which varies by jurisdiction and changes, and being wrong about it affects someone's rights. - Employment decisions. This records what people decided; it never decides. - A time clock with attendance verification, which is a hardware and trust problem of its own. - Mobile applications with push notifications, which is most of how staff actually use this category. ### README must contain - That this calculates no pay, checks no compliance and makes no employment decisions - That shifts are local wall clock commitments and survive clock changes - That availability and absence produce warnings requiring acknowledgement, never blocks - That every state change records a named individual, and the change log is append only - The retention policy, and how a subject access request is answered ===== AGENTS.md ===== # Agent instructions · When I Work indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with PostgreSQL, Self-hosted and private, not publicly indexed, A library with a full time zone database, storing instants plus the local zone, Append-only change records, never updated or deleted, CSV of hours worked, with no monetary calculation anywhere. 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 - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ===== BUILD_PLAN.md ===== # Build plan · When I Work A schedule for one team: shifts on a rota, availability submitted by staff, swap and cover requests that require a named approval, a change history nobody can quietly edit, and an export that a payroll system or an accountant can consume without this application interpreting 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 · Shifts on a rota, with time handled properly A published week shows the right shifts at the right local times. ### Steps 1. Model shifts with location zones Instant plus the local date. The zone belongs to the location, not the server. Files: `src/shifts.js` 2. Handle overnight shifts A shift crossing midnight belongs to the day it started, which is what staff and payroll both expect. Decide and document it. Files: `src/shifts.js` 3. Make publishing explicit A draft rota is not a commitment. Publishing is an action with an actor and a timestamp. Files: `src/publish.js` ### Done when - [ ] A week copied across a clock change keeps local times - [ ] Unpublished shifts are refused to staff accounts directly - [ ] An overnight shift's local date matches what staff would say - [ ] Publishing records who and when ### Watch out - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. ## Phase 2 · Availability, submitted by the people it describes Staff say when they can work, and the schedule warns when it contradicts them. ### Steps 1. Let staff submit their own availability It describes them, so they enter it. A manager editing it silently is the pattern to avoid. Files: `src/availability.js` 2. Apply from an effective date A change agreed in March applies from March, not to January's rota. Files: `src/availability.js` 3. Warn and record acknowledgement Never block. Record who acknowledged, because the record is the point. Files: `src/warnings.js` ### Done when - [ ] Availability applies from its effective date - [ ] An out-of-availability assignment warns and requires acknowledgement - [ ] The acknowledgement records who and when - [ ] Past warnings remain visible in history ### Watch out - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. ## Phase 3 · Swaps and cover, with a named approver Staff can arrange cover between themselves, and a manager approves it explicitly. ### Steps 1. Model the two-step flow Request, accept, approve. Each with an actor and a timestamp. Files: `src/swaps.js` 2. Leave the rota alone until approved A pending swap is not a schedule change. Verify the published rota is unaffected. Files: `src/swaps.js` 3. Record rejections with reasons A refused swap is as important a record as an approved one. Files: `src/swaps.js` ### Done when - [ ] A swap changes the rota only after acceptance and approval - [ ] A pending swap leaves the published rota unchanged - [ ] Every step records a named individual - [ ] A rejection is recorded with a reason ### Watch out - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. ## Phase 4 · Absence, recorded rather than adjudicated Time off is requested, decided by a person, and the schedule reflects it. ### Steps 1. Model requests and decisions Separate rows. A decision references a request rather than mutating it. Files: `src/timeoff.js` 2. Warn on conflicting assignment Approved absence is a strong warning, still not a block, and still acknowledged. Files: `src/warnings.js` 3. Record entitlement, never derive it A number a person entered from the contract. No accrual arithmetic anywhere. Files: `src/entitlement.js` ### Done when - [ ] An absence decision records a named person - [ ] A conflicting assignment warns and is acknowledged - [ ] No accrual or eligibility arithmetic exists in the code - [ ] A decision is superseded rather than edited ### Watch out - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. ## Phase 5 · Export, where the money is not calculated A payroll period exports as hours that somebody else turns into pay. ### Steps 1. Export hours and dates only No rates, no money, no classification. Grep the exporter for currency handling and find none. Files: `src/export.js` 2. Verify against a hand count One employee, one period, counted by hand. Arithmetic that nobody checked is arithmetic that is wrong. 3. Export the approval trail So a payroll query about a changed shift can be answered without opening the application. Files: `src/export.js` ### Done when - [ ] No monetary value or overtime classification appears in the export - [ ] The export matches a hand count for one employee - [ ] The approval trail exports alongside - [ ] Re-exporting an unchanged period is identical ### Watch out - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ## Not in this build - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. ## After v1, if you want it - Shift templates for a repeating weekly pattern, which saves most of the scheduling time - An open shift board staff can claim, still with an approval step - A coverage view showing gaps by position before publishing ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. This database holds personal data. DATABASE_URL=postgres://user:pass@localhost:5432/rota # Required. Time zone for a location. Shifts are local wall clock times in it. DEFAULT_LOCATION_TZ=Europe/London # Required. Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. OVERNIGHT_BELONGS_TO=start # Required. How long schedule records are kept, per your written policy. RETENTION_MONTHS_SCHEDULE=24 # Required. How long the change log is kept. Usually longer, because it is the record of decisions. RETENTION_MONTHS_AUDIT=72 # Required. Where the application is reachable. Private and not publicly indexed. PUBLIC_URL=https://rota.internal.example.com
You are building a production product version of When I Work. 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 ===== # When I Work · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For When I Work, build a small shift schedule, availability form, and swap approval workflow. The hard boundary is mobile apps, time clock, notifications, scheduling edge cases, and support, plus compliance, payroll, and regulated integrations. ## Product outcome A rota that keeps local times correct, warns rather than blocks, records a named person behind every decision, exports hours without touching money, and can answer what it holds about one employee. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - PostgreSQL - private self-hosted deployment - strict admin access - documented retention policy ## Explicit non-goals for v1 - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. - mobile apps, time clock, notifications, scheduling edge cases, and support - payroll rails - tax and labor-law compliance - benefits integrations - identity checks and global employment infrastructure ## Success criteria - A week copied across a clock change keeps local times - No monetary value or overtime classification appears anywhere in the code - An employee cannot read a colleague's data by direct request - The change log has no update or delete path - The subject access export produces everything about one person ===== BRIEF.md ===== # Build brief · When I Work 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 small shift schedule with availability, swaps and an approval trail, for one team to replace When I Work. 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 This schedules shifts and records approvals. It does not calculate pay, it does not check working time law, and it does not make employment decisions. Those three boundaries are not modesty; crossing any of them means your code is participating in a regulated decision about someone's livelihood, with the liability that implies. So it exports data for a payroll system rather than computing money, it warns about patterns rather than asserting compliance, and every consequential action carries a named human approver. Build it so the record shows who decided, not what the software decided. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · shift data is small, relational and consequential, which is what a real database is for - Deployment: Self-hosted and private, not publicly indexed · this holds staff names, availability and absence patterns - Time: A library with a full time zone database, storing instants plus the local zone · a shift is a local wall clock time and a clock change must not move it - Audit: Append-only change records, never updated or deleted · an approval trail that can be rewritten is not a trail - Export: CSV of hours worked, with no monetary calculation anywhere · payroll is somebody else's regulated responsibility and should stay there ### Data model (create this before Phase 1) - employees(id, name, email, role, employment_kind, active_from, active_to) · employment_kind is recorded, never interpreted - positions(id, name, colour), locations(id, name, timezone) - shifts(id, location_id, position_id, employee_id, starts_at_utc, ends_at_utc, local_date, state, published_at) - availability(id, employee_id, weekday, start_local, end_local, kind, effective_from) · kind is available, preferred or unavailable - time_off(id, employee_id, starts_on, ends_on, kind, state, decided_by, decided_at, note) - swaps(id, shift_id, from_employee_id, to_employee_id, state, requested_at, decided_by, decided_at) - changes(id, entity, entity_id, at, actor_id, before_json, after_json) · append only, no update or delete path - Every state change on a shift, swap or absence records who decided it. There is no automatic approval anywhere. ### Phase 1 · Shifts on a rota, with time handled properly Build: Create shifts against a location with its own time zone. Store the instant and the local date. A week view showing the rota. Publishing is an explicit action that marks shifts visible to staff. Trap: A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. Done when: A week copied across a daylight saving transition keeps its shifts at the same local times. Unpublished shifts are invisible to staff accounts, verified by requesting them directly. The local date on a shift matches the day staff would call it, including for an overnight shift. Publishing is recorded with who published and when. Do not build yet: No availability, swaps or exports. ### Phase 2 · Availability, submitted by the people it describes Build: Availability submitted by each employee with an effective date, so a change applies from when it was agreed rather than retroactively. When a shift is assigned outside someone's stated availability, warn the scheduler rather than blocking, and record that the warning was seen. Trap: Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. Done when: An employee submits availability and it applies from its effective date without altering past assignments. Assigning a shift outside availability produces a warning that must be acknowledged. The acknowledgement is recorded with who acknowledged it. A past week's warnings are visible in the history. Do not build yet: No swaps or absence yet. ### Phase 3 · Swaps and cover, with a named approver Build: A swap request naming the shift and the proposed cover. The receiving employee accepts, then a manager approves. Nothing changes the published rota until both have happened, and every step records who did it. Trap: Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. Done when: A swap requires acceptance and then approval before the rota changes. The rota is unchanged while a swap is pending. Each step records the individual who performed it. A rejected swap leaves the original assignment and records the rejection with a reason. Do not build yet: No absence or export yet. ### Phase 4 · Absence, recorded rather than adjudicated Build: Absence requests with a kind and dates. A manager decides, and the decision is recorded with who and when and any note. Approved absence blocks assignment with a warning. Record entitlement balances if you track them, but do not calculate eligibility. Trap: Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. Done when: An absence request is decided by a named person and the decision is recorded. Assigning a shift during approved absence warns and requires acknowledgement. Entitlement figures, if present, are recorded values that the application never computes, verified by inspecting the code for any accrual arithmetic. A decision cannot be edited, only superseded by a new one. Do not build yet: No export yet. ### Phase 5 · Export, where the money is not calculated Build: Export a period as CSV: employee, date, shift start and end, hours, position and any recorded notes. No rates, no totals in money, no overtime classification. Include the approval trail alongside so a query can be answered. Trap: Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. Done when: An export contains hours and dates with no monetary value and no overtime classification anywhere. The export matches the published rota for the period, verified against a hand count for one employee. The approval trail for the period exports alongside. Re-exporting an unchanged period produces identical output. Do not build yet: Operations and retention come last. ### Phase 6 · Access, retention and the records you must keep Build: Roles limiting staff to their own data and managers to their team. A written retention policy applied by a job. Backups. A subject access export producing everything held about one person. And a written statement of what this application does not do. Trap: Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. Done when: An employee can see their own shifts and not a colleague's, tested by requesting directly. A subject access export produces everything held about one person in a readable form. Retention removes data past the policy while keeping what employment records require. The README states plainly that this calculates no pay and makes no employment decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Payroll calculation of any kind. Hours out, money elsewhere, deliberately. - Working time and labour law compliance checking, which varies by jurisdiction and changes, and being wrong about it affects someone's rights. - Employment decisions. This records what people decided; it never decides. - A time clock with attendance verification, which is a hardware and trust problem of its own. - Mobile applications with push notifications, which is most of how staff actually use this category. ### README must contain - That this calculates no pay, checks no compliance and makes no employment decisions - That shifts are local wall clock commitments and survive clock changes - That availability and absence produce warnings requiring acknowledgement, never blocks - That every state change records a named individual, and the change log is append only - The retention policy, and how a subject access request is answered ===== ARCHITECTURE.md ===== # Architecture · When I Work ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | shift data is small, relational and consequential, which is what a real database is for | | Deployment | Self-hosted and private, not publicly indexed | this holds staff names, availability and absence patterns | | Time | A library with a full time zone database, storing instants plus the local zone | a shift is a local wall clock time and a clock change must not move it | | Audit | Append-only change records, never updated or deleted | an approval trail that can be rewritten is not a trail | | Export | CSV of hours worked, with no monetary calculation anywhere | payroll is somebody else's regulated responsibility and should stay there | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Shifts | The rota and its time handling. | The local wall clock rule is a correctness property and survives any change. | | Availability and absence | What staff said and what managers decided. | Warning rather than blocking is a deliberate product decision, not a missing feature. | | Approvals | Two-step flows with named actors. | Any implementation, provided no path auto-approves anything consequential. | | Audit | The append-only change log. | Append-only is the point. If a row can be updated, the trail is not a trail. | | Export | Hours out, money never. | Formats vary by payroll system. The absence of monetary calculation does not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. This database holds personal data. - `DEFAULT_LOCATION_TZ` · required · Time zone for a location. Shifts are local wall clock times in it. - `OVERNIGHT_BELONGS_TO` · required · Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. - `RETENTION_MONTHS_SCHEDULE` · required · How long schedule records are kept, per your written policy. - `RETENTION_MONTHS_AUDIT` · required · How long the change log is kept. Usually longer, because it is the record of decisions. - `PUBLIC_URL` · required · Where the application is reachable. Private and not publicly indexed. ## 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 · When I Work product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with PostgreSQL, Self-hosted and private, not publicly indexed, A library with a full time zone database, storing instants plus the local zone, Append-only change records, never updated or deleted, CSV of hours worked, with no monetary calculation anywhere. - 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 - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. - Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. ===== MILESTONES.md ===== # Delivery milestones · When I Work Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Shifts on a rota, with time handled properly A published week shows the right shifts at the right local times. ### Steps 1. Model shifts with location zones Instant plus the local date. The zone belongs to the location, not the server. Files: `src/shifts.js` 2. Handle overnight shifts A shift crossing midnight belongs to the day it started, which is what staff and payroll both expect. Decide and document it. Files: `src/shifts.js` 3. Make publishing explicit A draft rota is not a commitment. Publishing is an action with an actor and a timestamp. Files: `src/publish.js` ### Done when - [ ] A week copied across a clock change keeps local times - [ ] Unpublished shifts are refused to staff accounts directly - [ ] An overnight shift's local date matches what staff would say - [ ] Publishing records who and when ### Watch out - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. ## M2 · Availability, submitted by the people it describes Staff say when they can work, and the schedule warns when it contradicts them. ### Steps 1. Let staff submit their own availability It describes them, so they enter it. A manager editing it silently is the pattern to avoid. Files: `src/availability.js` 2. Apply from an effective date A change agreed in March applies from March, not to January's rota. Files: `src/availability.js` 3. Warn and record acknowledgement Never block. Record who acknowledged, because the record is the point. Files: `src/warnings.js` ### Done when - [ ] Availability applies from its effective date - [ ] An out-of-availability assignment warns and requires acknowledgement - [ ] The acknowledgement records who and when - [ ] Past warnings remain visible in history ### Watch out - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. ## M3 · Swaps and cover, with a named approver Staff can arrange cover between themselves, and a manager approves it explicitly. ### Steps 1. Model the two-step flow Request, accept, approve. Each with an actor and a timestamp. Files: `src/swaps.js` 2. Leave the rota alone until approved A pending swap is not a schedule change. Verify the published rota is unaffected. Files: `src/swaps.js` 3. Record rejections with reasons A refused swap is as important a record as an approved one. Files: `src/swaps.js` ### Done when - [ ] A swap changes the rota only after acceptance and approval - [ ] A pending swap leaves the published rota unchanged - [ ] Every step records a named individual - [ ] A rejection is recorded with a reason ### Watch out - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. ## M4 · Absence, recorded rather than adjudicated Time off is requested, decided by a person, and the schedule reflects it. ### Steps 1. Model requests and decisions Separate rows. A decision references a request rather than mutating it. Files: `src/timeoff.js` 2. Warn on conflicting assignment Approved absence is a strong warning, still not a block, and still acknowledged. Files: `src/warnings.js` 3. Record entitlement, never derive it A number a person entered from the contract. No accrual arithmetic anywhere. Files: `src/entitlement.js` ### Done when - [ ] An absence decision records a named person - [ ] A conflicting assignment warns and is acknowledged - [ ] No accrual or eligibility arithmetic exists in the code - [ ] A decision is superseded rather than edited ### Watch out - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. ## M5 · Export, where the money is not calculated A payroll period exports as hours that somebody else turns into pay. ### Steps 1. Export hours and dates only No rates, no money, no classification. Grep the exporter for currency handling and find none. Files: `src/export.js` 2. Verify against a hand count One employee, one period, counted by hand. Arithmetic that nobody checked is arithmetic that is wrong. 3. Export the approval trail So a payroll query about a changed shift can be answered without opening the application. Files: `src/export.js` ### Done when - [ ] No monetary value or overtime classification appears in the export - [ ] The export matches a hand count for one employee - [ ] The approval trail exports alongside - [ ] Re-exporting an unchanged period is identical ### Watch out - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ## M6 · Access, retention and the records you must keep (production only) Only the right people see staff data, and the retention policy is written and applied. ### Steps 1. Enforce roles server side Staff see themselves, managers see their team. Test with direct requests, not through the interface. Files: `src/auth.js` 2. Build the subject access export Everything about one person, readable. Before it is requested rather than after. Files: `src/sar.js` 3. Write and apply retention With the reason for each retention period, since employment records have their own requirements. Files: `docs/retention.md`, `src/retention.js` ### Done when - [ ] An employee cannot read a colleague's data through a direct request - [ ] A subject access export produces everything about one person - [ ] Retention applies the written policy - [ ] The README states what the application does not do ### Watch out - Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. ===== OPERATIONS.md ===== # Operations · When I Work ## Backup Nightly database backups off the machine, encrypted. This is personal data about employees and the backup carries the same obligations as the live database. ## Restore Restore and confirm a published week matches what staff saw, and that the change log is intact. An audit trail with a gap is worth knowing about immediately. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Backup success, and the count of unacknowledged warnings. A rising count of assignments made against availability without acknowledgement is a process problem the software can surface but not fix. ## Incident checklist If a schedule change is disputed, the append-only change log is the answer, which is why it has no update path. If personal data is exposed, the retention policy determines what was held and the subject access export determines what to disclose. 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 - [ ] A week copied across a clock change keeps local times - [ ] No monetary value or overtime classification appears anywhere in the code - [ ] An employee cannot read a colleague's data by direct request - [ ] The change log has no update or delete path - [ ] The subject access export produces everything about one person ## Launch constraint Do not market omitted When I Work 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 · secret. Your PostgreSQL connection string. This database holds personal data. DATABASE_URL=postgres://user:pass@localhost:5432/rota # Required. Time zone for a location. Shifts are local wall clock times in it. DEFAULT_LOCATION_TZ=Europe/London # Required. Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. OVERNIGHT_BELONGS_TO=start # Required. How long schedule records are kept, per your written policy. RETENTION_MONTHS_SCHEDULE=24 # Required. How long the change log is kept. Usually longer, because it is the record of decisions. RETENTION_MONTHS_AUDIT=72 # Required. Where the application is reachable. Private and not publicly indexed. PUBLIC_URL=https://rota.internal.example.com
# When I Work · indie build A schedule for one team: shifts on a rota, availability submitted by staff, swap and cover requests that require a named approval, a change history nobody can quietly edit, and an export that a payroll system or an accountant can consume without this application interpreting 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 | Node 22 with PostgreSQL | shift data is small, relational and consequential, which is what a real database is for | | Deployment | Self-hosted and private, not publicly indexed | this holds staff names, availability and absence patterns | | Time | A library with a full time zone database, storing instants plus the local zone | a shift is a local wall clock time and a clock change must not move it | | Audit | Append-only change records, never updated or deleted | an approval trail that can be rewritten is not a trail | | Export | CSV of hours worked, with no monetary calculation anywhere | payroll is somebody else's regulated responsibility and should stay there | ## 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. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Holds staff records and an append-only change log, both of which want a real database. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **An understanding of what you must not automate** · free - Why: Scheduling software sits next to employment law, and the boundary between recording a decision and making one is the whole design. Crossing it changes your responsibility. - Get it: Write down, before building, that this application will not calculate pay, will not assert compliance and will not decide anything. If your situation needs those, buy a product whose vendor carries that responsibility. - Verify: The boundary statement is written and appears in the README - [ ] **A retention policy for staff data** · free - Why: This holds personal data about identifiable people, including absence patterns. How long you keep it is a decision with legal weight in many places. - Get it: Decide the retention period for each kind of record and write the reason. Employment records often have a minimum retention that differs from the maximum you would otherwise want. - Verify: The policy is written with a reason per record kind - [ ] **A real rota for a real week** · free - Why: Overnight shifts, split shifts and someone who only works Tuesdays are all present in real rotas and absent from invented ones, and each breaks a naive model. - Get it: Use an actual week from the team this is for, including its awkward shifts. - Verify: A real week is entered including at least one overnight or split shift ## 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: - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. - mobile apps, time clock, notifications, scheduling edge cases, and support - payroll rails - tax and labor-law compliance - benefits integrations - identity checks and global employment infrastructure If one of those is essential to you, that is the reason to keep paying for When I Work, and the README should say so rather than pretend.
# Build brief · When I Work 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 small shift schedule with availability, swaps and an approval trail, for one team to replace When I Work. 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 This schedules shifts and records approvals. It does not calculate pay, it does not check working time law, and it does not make employment decisions. Those three boundaries are not modesty; crossing any of them means your code is participating in a regulated decision about someone's livelihood, with the liability that implies. So it exports data for a payroll system rather than computing money, it warns about patterns rather than asserting compliance, and every consequential action carries a named human approver. Build it so the record shows who decided, not what the software decided. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · shift data is small, relational and consequential, which is what a real database is for - Deployment: Self-hosted and private, not publicly indexed · this holds staff names, availability and absence patterns - Time: A library with a full time zone database, storing instants plus the local zone · a shift is a local wall clock time and a clock change must not move it - Audit: Append-only change records, never updated or deleted · an approval trail that can be rewritten is not a trail - Export: CSV of hours worked, with no monetary calculation anywhere · payroll is somebody else's regulated responsibility and should stay there ### Data model (create this before Phase 1) - employees(id, name, email, role, employment_kind, active_from, active_to) · employment_kind is recorded, never interpreted - positions(id, name, colour), locations(id, name, timezone) - shifts(id, location_id, position_id, employee_id, starts_at_utc, ends_at_utc, local_date, state, published_at) - availability(id, employee_id, weekday, start_local, end_local, kind, effective_from) · kind is available, preferred or unavailable - time_off(id, employee_id, starts_on, ends_on, kind, state, decided_by, decided_at, note) - swaps(id, shift_id, from_employee_id, to_employee_id, state, requested_at, decided_by, decided_at) - changes(id, entity, entity_id, at, actor_id, before_json, after_json) · append only, no update or delete path - Every state change on a shift, swap or absence records who decided it. There is no automatic approval anywhere. ### Phase 1 · Shifts on a rota, with time handled properly Build: Create shifts against a location with its own time zone. Store the instant and the local date. A week view showing the rota. Publishing is an explicit action that marks shifts visible to staff. Trap: A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. Done when: A week copied across a daylight saving transition keeps its shifts at the same local times. Unpublished shifts are invisible to staff accounts, verified by requesting them directly. The local date on a shift matches the day staff would call it, including for an overnight shift. Publishing is recorded with who published and when. Do not build yet: No availability, swaps or exports. ### Phase 2 · Availability, submitted by the people it describes Build: Availability submitted by each employee with an effective date, so a change applies from when it was agreed rather than retroactively. When a shift is assigned outside someone's stated availability, warn the scheduler rather than blocking, and record that the warning was seen. Trap: Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. Done when: An employee submits availability and it applies from its effective date without altering past assignments. Assigning a shift outside availability produces a warning that must be acknowledged. The acknowledgement is recorded with who acknowledged it. A past week's warnings are visible in the history. Do not build yet: No swaps or absence yet. ### Phase 3 · Swaps and cover, with a named approver Build: A swap request naming the shift and the proposed cover. The receiving employee accepts, then a manager approves. Nothing changes the published rota until both have happened, and every step records who did it. Trap: Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. Done when: A swap requires acceptance and then approval before the rota changes. The rota is unchanged while a swap is pending. Each step records the individual who performed it. A rejected swap leaves the original assignment and records the rejection with a reason. Do not build yet: No absence or export yet. ### Phase 4 · Absence, recorded rather than adjudicated Build: Absence requests with a kind and dates. A manager decides, and the decision is recorded with who and when and any note. Approved absence blocks assignment with a warning. Record entitlement balances if you track them, but do not calculate eligibility. Trap: Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. Done when: An absence request is decided by a named person and the decision is recorded. Assigning a shift during approved absence warns and requires acknowledgement. Entitlement figures, if present, are recorded values that the application never computes, verified by inspecting the code for any accrual arithmetic. A decision cannot be edited, only superseded by a new one. Do not build yet: No export yet. ### Phase 5 · Export, where the money is not calculated Build: Export a period as CSV: employee, date, shift start and end, hours, position and any recorded notes. No rates, no totals in money, no overtime classification. Include the approval trail alongside so a query can be answered. Trap: Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. Done when: An export contains hours and dates with no monetary value and no overtime classification anywhere. The export matches the published rota for the period, verified against a hand count for one employee. The approval trail for the period exports alongside. Re-exporting an unchanged period produces identical output. Do not build yet: Operations and retention come last. ### Phase 6 · Access, retention and the records you must keep Build: Roles limiting staff to their own data and managers to their team. A written retention policy applied by a job. Backups. A subject access export producing everything held about one person. And a written statement of what this application does not do. Trap: Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. Done when: An employee can see their own shifts and not a colleague's, tested by requesting directly. A subject access export produces everything held about one person in a readable form. Retention removes data past the policy while keeping what employment records require. The README states plainly that this calculates no pay and makes no employment decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Payroll calculation of any kind. Hours out, money elsewhere, deliberately. - Working time and labour law compliance checking, which varies by jurisdiction and changes, and being wrong about it affects someone's rights. - Employment decisions. This records what people decided; it never decides. - A time clock with attendance verification, which is a hardware and trust problem of its own. - Mobile applications with push notifications, which is most of how staff actually use this category. ### README must contain - That this calculates no pay, checks no compliance and makes no employment decisions - That shifts are local wall clock commitments and survive clock changes - That availability and absence produce warnings requiring acknowledgement, never blocks - That every state change records a named individual, and the change log is append only - The retention policy, and how a subject access request is answered
# Agent instructions · When I Work indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with PostgreSQL, Self-hosted and private, not publicly indexed, A library with a full time zone database, storing instants plus the local zone, Append-only change records, never updated or deleted, CSV of hours worked, with no monetary calculation anywhere. 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 - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification.
# Build plan · When I Work A schedule for one team: shifts on a rota, availability submitted by staff, swap and cover requests that require a named approval, a change history nobody can quietly edit, and an export that a payroll system or an accountant can consume without this application interpreting 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 · Shifts on a rota, with time handled properly A published week shows the right shifts at the right local times. ### Steps 1. Model shifts with location zones Instant plus the local date. The zone belongs to the location, not the server. Files: `src/shifts.js` 2. Handle overnight shifts A shift crossing midnight belongs to the day it started, which is what staff and payroll both expect. Decide and document it. Files: `src/shifts.js` 3. Make publishing explicit A draft rota is not a commitment. Publishing is an action with an actor and a timestamp. Files: `src/publish.js` ### Done when - [ ] A week copied across a clock change keeps local times - [ ] Unpublished shifts are refused to staff accounts directly - [ ] An overnight shift's local date matches what staff would say - [ ] Publishing records who and when ### Watch out - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. ## Phase 2 · Availability, submitted by the people it describes Staff say when they can work, and the schedule warns when it contradicts them. ### Steps 1. Let staff submit their own availability It describes them, so they enter it. A manager editing it silently is the pattern to avoid. Files: `src/availability.js` 2. Apply from an effective date A change agreed in March applies from March, not to January's rota. Files: `src/availability.js` 3. Warn and record acknowledgement Never block. Record who acknowledged, because the record is the point. Files: `src/warnings.js` ### Done when - [ ] Availability applies from its effective date - [ ] An out-of-availability assignment warns and requires acknowledgement - [ ] The acknowledgement records who and when - [ ] Past warnings remain visible in history ### Watch out - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. ## Phase 3 · Swaps and cover, with a named approver Staff can arrange cover between themselves, and a manager approves it explicitly. ### Steps 1. Model the two-step flow Request, accept, approve. Each with an actor and a timestamp. Files: `src/swaps.js` 2. Leave the rota alone until approved A pending swap is not a schedule change. Verify the published rota is unaffected. Files: `src/swaps.js` 3. Record rejections with reasons A refused swap is as important a record as an approved one. Files: `src/swaps.js` ### Done when - [ ] A swap changes the rota only after acceptance and approval - [ ] A pending swap leaves the published rota unchanged - [ ] Every step records a named individual - [ ] A rejection is recorded with a reason ### Watch out - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. ## Phase 4 · Absence, recorded rather than adjudicated Time off is requested, decided by a person, and the schedule reflects it. ### Steps 1. Model requests and decisions Separate rows. A decision references a request rather than mutating it. Files: `src/timeoff.js` 2. Warn on conflicting assignment Approved absence is a strong warning, still not a block, and still acknowledged. Files: `src/warnings.js` 3. Record entitlement, never derive it A number a person entered from the contract. No accrual arithmetic anywhere. Files: `src/entitlement.js` ### Done when - [ ] An absence decision records a named person - [ ] A conflicting assignment warns and is acknowledged - [ ] No accrual or eligibility arithmetic exists in the code - [ ] A decision is superseded rather than edited ### Watch out - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. ## Phase 5 · Export, where the money is not calculated A payroll period exports as hours that somebody else turns into pay. ### Steps 1. Export hours and dates only No rates, no money, no classification. Grep the exporter for currency handling and find none. Files: `src/export.js` 2. Verify against a hand count One employee, one period, counted by hand. Arithmetic that nobody checked is arithmetic that is wrong. 3. Export the approval trail So a payroll query about a changed shift can be answered without opening the application. Files: `src/export.js` ### Done when - [ ] No monetary value or overtime classification appears in the export - [ ] The export matches a hand count for one employee - [ ] The approval trail exports alongside - [ ] Re-exporting an unchanged period is identical ### Watch out - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ## Not in this build - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. ## After v1, if you want it - Shift templates for a repeating weekly pattern, which saves most of the scheduling time - An open shift board staff can claim, still with an approval step - A coverage view showing gaps by position before publishing
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. This database holds personal data. DATABASE_URL=postgres://user:pass@localhost:5432/rota # Required. Time zone for a location. Shifts are local wall clock times in it. DEFAULT_LOCATION_TZ=Europe/London # Required. Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. OVERNIGHT_BELONGS_TO=start # Required. How long schedule records are kept, per your written policy. RETENTION_MONTHS_SCHEDULE=24 # Required. How long the change log is kept. Usually longer, because it is the record of decisions. RETENTION_MONTHS_AUDIT=72 # Required. Where the application is reachable. Private and not publicly indexed. PUBLIC_URL=https://rota.internal.example.com
# When I Work · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For When I Work, build a small shift schedule, availability form, and swap approval workflow. The hard boundary is mobile apps, time clock, notifications, scheduling edge cases, and support, plus compliance, payroll, and regulated integrations. ## Product outcome A rota that keeps local times correct, warns rather than blocks, records a named person behind every decision, exports hours without touching money, and can answer what it holds about one employee. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - PostgreSQL - private self-hosted deployment - strict admin access - documented retention policy ## Explicit non-goals for v1 - Payroll calculation. - Working time and labour law compliance checking. - Any employment decision. - A time clock with attendance verification. - Mobile applications with notifications. - mobile apps, time clock, notifications, scheduling edge cases, and support - payroll rails - tax and labor-law compliance - benefits integrations - identity checks and global employment infrastructure ## Success criteria - A week copied across a clock change keeps local times - No monetary value or overtime classification appears anywhere in the code - An employee cannot read a colleague's data by direct request - The change log has no update or delete path - The subject access export produces everything about one person
# Build brief · When I Work 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 small shift schedule with availability, swaps and an approval trail, for one team to replace When I Work. 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 This schedules shifts and records approvals. It does not calculate pay, it does not check working time law, and it does not make employment decisions. Those three boundaries are not modesty; crossing any of them means your code is participating in a regulated decision about someone's livelihood, with the liability that implies. So it exports data for a payroll system rather than computing money, it warns about patterns rather than asserting compliance, and every consequential action carries a named human approver. Build it so the record shows who decided, not what the software decided. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · shift data is small, relational and consequential, which is what a real database is for - Deployment: Self-hosted and private, not publicly indexed · this holds staff names, availability and absence patterns - Time: A library with a full time zone database, storing instants plus the local zone · a shift is a local wall clock time and a clock change must not move it - Audit: Append-only change records, never updated or deleted · an approval trail that can be rewritten is not a trail - Export: CSV of hours worked, with no monetary calculation anywhere · payroll is somebody else's regulated responsibility and should stay there ### Data model (create this before Phase 1) - employees(id, name, email, role, employment_kind, active_from, active_to) · employment_kind is recorded, never interpreted - positions(id, name, colour), locations(id, name, timezone) - shifts(id, location_id, position_id, employee_id, starts_at_utc, ends_at_utc, local_date, state, published_at) - availability(id, employee_id, weekday, start_local, end_local, kind, effective_from) · kind is available, preferred or unavailable - time_off(id, employee_id, starts_on, ends_on, kind, state, decided_by, decided_at, note) - swaps(id, shift_id, from_employee_id, to_employee_id, state, requested_at, decided_by, decided_at) - changes(id, entity, entity_id, at, actor_id, before_json, after_json) · append only, no update or delete path - Every state change on a shift, swap or absence records who decided it. There is no automatic approval anywhere. ### Phase 1 · Shifts on a rota, with time handled properly Build: Create shifts against a location with its own time zone. Store the instant and the local date. A week view showing the rota. Publishing is an explicit action that marks shifts visible to staff. Trap: A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. Done when: A week copied across a daylight saving transition keeps its shifts at the same local times. Unpublished shifts are invisible to staff accounts, verified by requesting them directly. The local date on a shift matches the day staff would call it, including for an overnight shift. Publishing is recorded with who published and when. Do not build yet: No availability, swaps or exports. ### Phase 2 · Availability, submitted by the people it describes Build: Availability submitted by each employee with an effective date, so a change applies from when it was agreed rather than retroactively. When a shift is assigned outside someone's stated availability, warn the scheduler rather than blocking, and record that the warning was seen. Trap: Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. Done when: An employee submits availability and it applies from its effective date without altering past assignments. Assigning a shift outside availability produces a warning that must be acknowledged. The acknowledgement is recorded with who acknowledged it. A past week's warnings are visible in the history. Do not build yet: No swaps or absence yet. ### Phase 3 · Swaps and cover, with a named approver Build: A swap request naming the shift and the proposed cover. The receiving employee accepts, then a manager approves. Nothing changes the published rota until both have happened, and every step records who did it. Trap: Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. Done when: A swap requires acceptance and then approval before the rota changes. The rota is unchanged while a swap is pending. Each step records the individual who performed it. A rejected swap leaves the original assignment and records the rejection with a reason. Do not build yet: No absence or export yet. ### Phase 4 · Absence, recorded rather than adjudicated Build: Absence requests with a kind and dates. A manager decides, and the decision is recorded with who and when and any note. Approved absence blocks assignment with a warning. Record entitlement balances if you track them, but do not calculate eligibility. Trap: Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. Done when: An absence request is decided by a named person and the decision is recorded. Assigning a shift during approved absence warns and requires acknowledgement. Entitlement figures, if present, are recorded values that the application never computes, verified by inspecting the code for any accrual arithmetic. A decision cannot be edited, only superseded by a new one. Do not build yet: No export yet. ### Phase 5 · Export, where the money is not calculated Build: Export a period as CSV: employee, date, shift start and end, hours, position and any recorded notes. No rates, no totals in money, no overtime classification. Include the approval trail alongside so a query can be answered. Trap: Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. Done when: An export contains hours and dates with no monetary value and no overtime classification anywhere. The export matches the published rota for the period, verified against a hand count for one employee. The approval trail for the period exports alongside. Re-exporting an unchanged period produces identical output. Do not build yet: Operations and retention come last. ### Phase 6 · Access, retention and the records you must keep Build: Roles limiting staff to their own data and managers to their team. A written retention policy applied by a job. Backups. A subject access export producing everything held about one person. And a written statement of what this application does not do. Trap: Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks. Done when: An employee can see their own shifts and not a colleague's, tested by requesting directly. A subject access export produces everything held about one person in a readable form. Retention removes data past the policy while keeping what employment records require. The README states plainly that this calculates no pay and makes no employment decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Payroll calculation of any kind. Hours out, money elsewhere, deliberately. - Working time and labour law compliance checking, which varies by jurisdiction and changes, and being wrong about it affects someone's rights. - Employment decisions. This records what people decided; it never decides. - A time clock with attendance verification, which is a hardware and trust problem of its own. - Mobile applications with push notifications, which is most of how staff actually use this category. ### README must contain - That this calculates no pay, checks no compliance and makes no employment decisions - That shifts are local wall clock commitments and survive clock changes - That availability and absence produce warnings requiring acknowledgement, never blocks - That every state change records a named individual, and the change log is append only - The retention policy, and how a subject access request is answered
# Architecture · When I Work ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | shift data is small, relational and consequential, which is what a real database is for | | Deployment | Self-hosted and private, not publicly indexed | this holds staff names, availability and absence patterns | | Time | A library with a full time zone database, storing instants plus the local zone | a shift is a local wall clock time and a clock change must not move it | | Audit | Append-only change records, never updated or deleted | an approval trail that can be rewritten is not a trail | | Export | CSV of hours worked, with no monetary calculation anywhere | payroll is somebody else's regulated responsibility and should stay there | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Shifts | The rota and its time handling. | The local wall clock rule is a correctness property and survives any change. | | Availability and absence | What staff said and what managers decided. | Warning rather than blocking is a deliberate product decision, not a missing feature. | | Approvals | Two-step flows with named actors. | Any implementation, provided no path auto-approves anything consequential. | | Audit | The append-only change log. | Append-only is the point. If a row can be updated, the trail is not a trail. | | Export | Hours out, money never. | Formats vary by payroll system. The absence of monetary calculation does not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. This database holds personal data. - `DEFAULT_LOCATION_TZ` · required · Time zone for a location. Shifts are local wall clock times in it. - `OVERNIGHT_BELONGS_TO` · required · Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. - `RETENTION_MONTHS_SCHEDULE` · required · How long schedule records are kept, per your written policy. - `RETENTION_MONTHS_AUDIT` · required · How long the change log is kept. Usually longer, because it is the record of decisions. - `PUBLIC_URL` · required · Where the application is reachable. Private and not publicly indexed. ## 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 · When I Work product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with PostgreSQL, Self-hosted and private, not publicly indexed, A library with a full time zone database, storing instants plus the local zone, Append-only change records, never updated or deleted, CSV of hours worked, with no monetary calculation anywhere. - 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 - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. - Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks.
# Delivery milestones · When I Work Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Shifts on a rota, with time handled properly A published week shows the right shifts at the right local times. ### Steps 1. Model shifts with location zones Instant plus the local date. The zone belongs to the location, not the server. Files: `src/shifts.js` 2. Handle overnight shifts A shift crossing midnight belongs to the day it started, which is what staff and payroll both expect. Decide and document it. Files: `src/shifts.js` 3. Make publishing explicit A draft rota is not a commitment. Publishing is an action with an actor and a timestamp. Files: `src/publish.js` ### Done when - [ ] A week copied across a clock change keeps local times - [ ] Unpublished shifts are refused to staff accounts directly - [ ] An overnight shift's local date matches what staff would say - [ ] Publishing records who and when ### Watch out - A shift is a local wall clock commitment: an eight in the morning shift stays at eight after a clock change. Store the instant computed from the local time and the location's zone, and recompute rather than shifting instants when a schedule is copied to another week that crosses a transition. ## M2 · Availability, submitted by the people it describes Staff say when they can work, and the schedule warns when it contradicts them. ### Steps 1. Let staff submit their own availability It describes them, so they enter it. A manager editing it silently is the pattern to avoid. Files: `src/availability.js` 2. Apply from an effective date A change agreed in March applies from March, not to January's rota. Files: `src/availability.js` 3. Warn and record acknowledgement Never block. Record who acknowledged, because the record is the point. Files: `src/warnings.js` ### Done when - [ ] Availability applies from its effective date - [ ] An out-of-availability assignment warns and requires acknowledgement - [ ] The acknowledgement records who and when - [ ] Past warnings remain visible in history ### Watch out - Blocking an assignment outside availability sounds protective and is wrong, because there are legitimate reasons to ask and the manager needs to be able to. Warn, require acknowledgement, and record it, so the conversation happens and the record shows it did. ## M3 · Swaps and cover, with a named approver Staff can arrange cover between themselves, and a manager approves it explicitly. ### Steps 1. Model the two-step flow Request, accept, approve. Each with an actor and a timestamp. Files: `src/swaps.js` 2. Leave the rota alone until approved A pending swap is not a schedule change. Verify the published rota is unaffected. Files: `src/swaps.js` 3. Record rejections with reasons A refused swap is as important a record as an approved one. Files: `src/swaps.js` ### Done when - [ ] A swap changes the rota only after acceptance and approval - [ ] A pending swap leaves the published rota unchanged - [ ] Every step records a named individual - [ ] A rejection is recorded with a reason ### Watch out - Auto-approving a swap when both employees agree removes the manager from a decision that affects coverage and cost, and it is exactly the record that matters when something goes wrong. Keep the approval step, and make the approver a named person rather than a role. ## M4 · Absence, recorded rather than adjudicated Time off is requested, decided by a person, and the schedule reflects it. ### Steps 1. Model requests and decisions Separate rows. A decision references a request rather than mutating it. Files: `src/timeoff.js` 2. Warn on conflicting assignment Approved absence is a strong warning, still not a block, and still acknowledged. Files: `src/warnings.js` 3. Record entitlement, never derive it A number a person entered from the contract. No accrual arithmetic anywhere. Files: `src/entitlement.js` ### Done when - [ ] An absence decision records a named person - [ ] A conflicting assignment warns and is acknowledged - [ ] No accrual or eligibility arithmetic exists in the code - [ ] A decision is superseded rather than edited ### Watch out - Calculating leave entitlement means encoding employment terms and statutory rules that vary by jurisdiction, contract and length of service, and getting it wrong affects someone's actual leave. Record balances as figures a person entered from the contract, and never derive them. ## M5 · Export, where the money is not calculated A payroll period exports as hours that somebody else turns into pay. ### Steps 1. Export hours and dates only No rates, no money, no classification. Grep the exporter for currency handling and find none. Files: `src/export.js` 2. Verify against a hand count One employee, one period, counted by hand. Arithmetic that nobody checked is arithmetic that is wrong. 3. Export the approval trail So a payroll query about a changed shift can be answered without opening the application. Files: `src/export.js` ### Done when - [ ] No monetary value or overtime classification appears in the export - [ ] The export matches a hand count for one employee - [ ] The approval trail exports alongside - [ ] Re-exporting an unchanged period is identical ### Watch out - Classifying hours as overtime is a legal determination that depends on jurisdiction, contract and averaging periods, and an incorrect classification underpays someone. Export raw hours with their dates and let the payroll system, which is built and maintained for that, do the classification. ## M6 · Access, retention and the records you must keep (production only) Only the right people see staff data, and the retention policy is written and applied. ### Steps 1. Enforce roles server side Staff see themselves, managers see their team. Test with direct requests, not through the interface. Files: `src/auth.js` 2. Build the subject access export Everything about one person, readable. Before it is requested rather than after. Files: `src/sar.js` 3. Write and apply retention With the reason for each retention period, since employment records have their own requirements. Files: `docs/retention.md`, `src/retention.js` ### Done when - [ ] An employee cannot read a colleague's data through a direct request - [ ] A subject access export produces everything about one person - [ ] Retention applies the written policy - [ ] The README states what the application does not do ### Watch out - Staff scheduling data is personal data about identifiable people, including patterns of absence, and in many jurisdictions employees have rights to access and correct it. A subject access request answered by manual database queries takes a day and is easy to get incomplete. Build the export once, before anyone asks.
# Operations · When I Work ## Backup Nightly database backups off the machine, encrypted. This is personal data about employees and the backup carries the same obligations as the live database. ## Restore Restore and confirm a published week matches what staff saw, and that the change log is intact. An audit trail with a gap is worth knowing about immediately. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Backup success, and the count of unacknowledged warnings. A rising count of assignments made against availability without acknowledgement is a process problem the software can surface but not fix. ## Incident checklist If a schedule change is disputed, the append-only change log is the answer, which is why it has no update path. If personal data is exposed, the retention policy determines what was held and the subject access export determines what to disclose. 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 - [ ] A week copied across a clock change keeps local times - [ ] No monetary value or overtime classification appears anywhere in the code - [ ] An employee cannot read a colleague's data by direct request - [ ] The change log has no update or delete path - [ ] The subject access export produces everything about one person ## Launch constraint Do not market omitted When I Work 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 · secret. Your PostgreSQL connection string. This database holds personal data. DATABASE_URL=postgres://user:pass@localhost:5432/rota # Required. Time zone for a location. Shifts are local wall clock times in it. DEFAULT_LOCATION_TZ=Europe/London # Required. Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. OVERNIGHT_BELONGS_TO=start # Required. How long schedule records are kept, per your written policy. RETENTION_MONTHS_SCHEDULE=24 # Required. How long the change log is kept. Usually longer, because it is the record of decisions. RETENTION_MONTHS_AUDIT=72 # Required. Where the application is reachable. Private and not publicly indexed. PUBLIC_URL=https://rota.internal.example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for When I Work because HR platforms survive because mistakes affect pay, benefits, immigration, and legal obligations, not because forms are difficult to render. The recurring cost buys sensitive data, access, retention, audit, payroll calculations, filings, benefits, employment law, incident response, and support, not just the visible interface.
xmobile apps, time clock, notifications, scheduling edge cases, and support
xpayroll rails
xtax and labor-law compliance
xbenefits integrations
xidentity checks and global employment infrastructure
Don't feel like building it? These folks already made it free.
all 3 free alternatives to When I Work →· no votes, no pay-to-list · just what's real
When I Work pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| essentials | $2.50/user | $2.38/user | Scheduling and team messaging; price per user/month |
| pro | $5/user | $4.75/user | Expanded scheduling, controls, and reporting; price per user/month |
| premium | $8/user | $7.60/user | Advanced scheduling and workforce controls; price per user/month |
free tierno free tier; 14-day trial with no card
billingmonthly + annual (annual is prepaid and 5% lower)
hidden costsTime Tracking & Attendance is a separately toggled add-on with no public price on the live plan table. Annual plans cannot reduce seats or downgrade until within 30 days of renewal, use manual renewal, and are non-refundable.
verified 2026-08-10 · source ↗
Vibecode When I Work
Kinda. The core of When I Work is buildable in a weekend with the prompt on this page, but there are real gaps: mobile apps, time clock, notifications, scheduling edge cases, and support, payroll rails. Read the honest list above before committing.
How much does When I Work cost?
When I Work costs about $2.5/month (Essentials, checked 2026-07-31), which is $30 per year.
What do I lose by replacing When I Work?
Honestly: mobile apps, time clock, notifications, scheduling edge cases, and support; payroll rails; tax and labor-law compliance; benefits integrations; identity checks and global employment infrastructure. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to When I Work?
Yes: Sling Free (Schedules, availability and manager-approved swaps for 30 people; the time clock costs extra.) 7shifts Comp (A small roster, availability form and approved shift trades for one 15-person location.) Connecteam Small Business Plan (Scheduling, availability and shift changes for ten people; employee eleven ends the free plan.) All 3 curated free alternatives are at vibecodeit.com/when-i-work/alternatives. The prompt is for when you want it exactly your way.