Build When I Work
KINDAreplaces $2.5/mosaves $30/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
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. open ↗
Verify
psql connects and reports 16 or higher - decidefree
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 - decidefree
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 - have readyfree
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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:pass@localhost:5432/rota | Your PostgreSQL connection string. This database holds personal data. |
DEFAULT_LOCATION_TZ | required | Europe/London | Time zone for a location. Shifts are local wall clock times in it. |
OVERNIGHT_BELONGS_TO | required | start | Whether a shift crossing midnight counts on its start or end date. State it, because payroll will ask. |
RETENTION_MONTHS_SCHEDULE | required | 24 | How long schedule records are kept, per your written policy. |
RETENTION_MONTHS_AUDIT | required | 72 | How long the change log is kept. Usually longer, because it is the record of decisions. |
PUBLIC_URL | required | https://rota.internal.example.com | Where the application is reachable. Private and not publicly indexed. |
The build, in order
Shifts on a rota, with time handled properly
A published week shows the right shifts at the right local times.
Instant plus the local date. The zone belongs to the location, not the server.
Files
src/shifts.jsA shift crossing midnight belongs to the day it started, which is what staff and payroll both expect. Decide and document it.
Files
src/shifts.jsA draft rota is not a commitment. Publishing is an action with an actor and a timestamp.
Files
src/publish.js
done when · tick each as it passeswatch 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.
Availability, submitted by the people it describes
Staff say when they can work, and the schedule warns when it contradicts them.
It describes them, so they enter it. A manager editing it silently is the pattern to avoid.
Files
src/availability.jsA change agreed in March applies from March, not to January's rota.
Files
src/availability.jsNever block. Record who acknowledged, because the record is the point.
Files
src/warnings.js
done when · tick each as it passeswatch 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.
Swaps and cover, with a named approver
Staff can arrange cover between themselves, and a manager approves it explicitly.
Request, accept, approve. Each with an actor and a timestamp.
Files
src/swaps.jsA pending swap is not a schedule change. Verify the published rota is unaffected.
Files
src/swaps.jsA refused swap is as important a record as an approved one.
Files
src/swaps.js
done when · tick each as it passeswatch 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.
Absence, recorded rather than adjudicated
Time off is requested, decided by a person, and the schedule reflects it.
Separate rows. A decision references a request rather than mutating it.
Files
src/timeoff.jsApproved absence is a strong warning, still not a block, and still acknowledged.
Files
src/warnings.jsA number a person entered from the contract. No accrual arithmetic anywhere.
Files
src/entitlement.js
done when · tick each as it passeswatch 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.
Export, where the money is not calculated
A payroll period exports as hours that somebody else turns into pay.
No rates, no money, no classification. Grep the exporter for currency handling and find none.
Files
src/export.jsOne employee, one period, counted by hand. Arithmetic that nobody checked is arithmetic that is wrong.
So a payroll query about a changed shift can be answered without opening the application.
Files
src/export.js
done when · tick each as it passeswatch 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.
Access, retention and the records you must keepproduct builder
Only the right people see staff data, and the retention policy is written and applied.
Staff see themselves, managers see their team. Test with direct requests, not through the interface.
Files
src/auth.jsEverything about one person, readable. Before it is requested rather than after.
Files
src/sar.jsWith the reason for each retention period, since employment records have their own requirements.
Files
docs/retention.mdsrc/retention.js
done when · tick each as it passeswatch 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.
That is the whole plan for When I Work. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more hr.