Skip to content
All work
Software DevelopmentCommercial Projects

HR & Workforce Platform

What began as a way to record hours became a full workforce management system: people, scheduling, absence, documents, payroll inputs and reporting, built as independent modules on a shared authentication and permissions core. Attendance remained its engineering centre — time arrives from four different places and has to leave as one figure somebody can be paid on.

Role
Architect and full-stack engineer
Timeline
Sept 2025 — Present
Status
in progress
ReactTypeScriptViteTailwindCSSExpressMongoDBBetter-AuthNode.jsVercelRailway
modules
13on one shared core
API routers
22over 26 data models
time sources to one figure
4 → 1reconciled per person per day

The problem

Small organisations run HR on a stack of disconnected tools — a spreadsheet for hours, email for absence requests, a folder for contracts. Each is individually fine and collectively unauditable. There is no single answer to who worked what, when, and under which agreement. Attendance is the sharpest version of the problem, because the answer has to survive being disagreed with: somebody is paid on it, and occasionally somebody is disciplined on it.

Design

Thirteen modules over one core. Authentication, roles, the employee record and the organisation's working rules are shared; everything else — attendance, leave, documents, expenses, assets, performance, recruitment, onboarding, learning, payslips — owns its own routes, models and screens, and reads the core rather than reaching into its neighbours. An organisation switches modules off at the API rather than only in the menu: a module that is off refuses the requests behind it, including the public careers portal when recruitment is disabled. That boundary is what made it possible to start with time logging and keep adding without a rewrite.

Architecture

Thirteen modules sit on one shared core — Better-Auth sessions and roles, the employee record, departments, and the organisation's working rules. Attendance is drawn in full because it is the module the others were shaped around: time arrives from four different places and has to come out as one defensible figure. Live punches and imported clocking-terminal records are reconciled when a report is read rather than by rewriting either source, so neither can quietly overwrite the other, and every clocking spell survives into the audit trail.

Walterstone architecture: thirteen modules on a shared core of authentication, employee records and working rules, with the attendance module expanded — time entering from the in-app clock, a geofence-verified NFC tap, imported terminal files and approved corrections, reconciled at read time and judged against per-person working rules, producing the attendance dashboard, timesheets, period reports and payroll exports.

Challenges

  1. 01

    Two sources of truth that must not overwrite each other

    Hours arrive from the app and from a clocking terminal, in incompatible shapes: one is a session with exact timestamps, the other a day already summarised to decimal hours. Copying either into the other loses something — a live shift written into the imported collection reads as a missed punch, and an import written over a live day destroys the clock times. They are reconciled at read time instead, into one row per person per day that keeps both sources and tags the days where they disagree, so a discrepancy can be seen rather than silently resolved.

  2. 02

    Hours that have to be exact

    Seven hours twenty minutes is 7.333… recurring. Carrying decimal hours through a month of additions drifts by minutes, and minutes are pay. Worked time is held in whole minutes everywhere and the decimal is derived only at the point of export, beside the minute column rather than instead of it, so neither payroll software nor a human reading the sheet has to trust a rounded number.

  3. 03

    The same rule, written down three times

    What counts as an absence was defined separately in the dashboard, the printed reports and the team report. When booked leave became a third outcome — neither worked nor absent — only one of the three was corrected. The other two went on reporting approved holiday as unexplained absence, in exactly the documents a manager prints and takes into a meeting. The rule now exists once; the lesson was that three copies will diverge and the one you fix is rarely the one somebody reads.

  4. 04

    Proving somebody is actually on site

    A tag on a wall proves a phone touched a sticker, not that a person is at work — and a web page cannot read a tag's hardware identity anyway. So the tag only says which workplace, and the claim is tested separately: the device reports a position, the server recomputes the distance from the site's own coordinates, and a reported accuracy worse than the boundary is answered with 'cannot tell' rather than a guess in either direction. The device's own margin is never added to the radius, because that margin is a number the client chooses.

The solution

A React and TypeScript front end against an Express and MongoDB API, with Better-Auth handling sessions and role-based access. The client is served from Vercel and rewrites its own /api path to the API on Railway, so the session cookie stays first-party and the browsers that block third-party cookies by default do not quietly sign people out. Uploaded files live in the same database as the records, so one backup covers both.

Result

In active use and still being extended. All thirteen modules are built rather than scaffolded; the recent work has been on the parts that decide money — configurable working patterns per person, department and organisation, pay periods that match the cycle the organisation actually runs, and location-verified clocking in. The honest edge of it is that the automatic clock-out on leaving a site is built and tested on the server but waits on a native mobile client, because a browser tab cannot watch a boundary once it is closed.

What I took from it

  • The module boundary earns its cost the third time you add a feature, not the first.

  • Scope growth is not a problem if the architecture anticipated it. The rewrite you avoid is the return on that design work.

  • A rule written down twice is a rule that will disagree with itself. The second copy is cheaper to delete than the bug it causes is to find.

  • A setting enforced only in the interface is not a setting, it is a suggestion. The module switches hid menus long before the server was taught to refuse the routes behind them.

  • Where the software decides something about a person — late, absent, owed overtime — it has to be able to show its working. Reconciling at read time costs a little performance and buys an answer you can defend.