Work

ShiftSync

A private web app that reads a published roster file and writes every shift straight into the calendar its owner already uses.

Web, installable Google Calendar Microsoft Outlook iCal feed

The problem

People on rotating rosters are handed their schedule as a file: a spreadsheet-style export, or a PDF pulled out of a central rostering system. Neither is something a calendar can read. To actually know when they are working, they either squint at that document on a phone or retype four weeks of shifts into their personal calendar by hand, one all-day event at a time.

Rosters then change after publication, so the hand-typed copy silently goes stale, and the next 28-day cycle means doing the whole job again. Checking whether a published roster even complies with the rules that govern it (minimum rest days, limits on consecutive shifts and weekends worked, expected total hours) means counting rows manually, which almost nobody does.

What was built

ShiftSync ingests a roster file and writes each shift straight into the user's own calendar. Access is closed: sign-in is passwordless and restricted to verified work email addresses on one approved domain, proven with a 6-digit email code or an emailed magic link, after which the server mints its own signed session cookie.

A roster reaches the app one of two ways. It can be dragged into the browser as a file, or emailed as an attachment to a dedicated inbound address handled by a serverless email worker that authenticates the message and verifies the sender before importing anything.

Parsed shifts are classified by type, checked against the roster rules that apply to them, previewed, and only then written to Google Calendar or Microsoft Outlook over OAuth scoped to calendars only: no mail, contacts or files. Users with no connected provider instead get a self-hosted iCal or webcal feed they can subscribe to from Apple Calendar and similar apps. Every import is recorded with a one-tap undo that removes those events again.

Platform

Nothing here is rented from a third party that did not have to be. The dependency list is short on purpose, and the pieces that hold user data are ones we run ourselves.

Frontend
React with React Router, built by Vite, shipped as an installable PWA with a versioned service worker. Each main panel is code-split at the route level, so admin-only code never ships to anyone else.
Backend
Node.js with Express, with standard hardening middleware and request rate limiting in place.
Sign-in
Passwordless via Stytch, using either a 6-digit email code or a magic link. The server then issues its own signed session, with the ability to sign an account out of every device at once.
Calendars
Google Calendar API v3 and Microsoft Graph v1.0 over OAuth, with refresh tokens encrypted at rest, plus a self-hosted .ics/webcal feed the user can revoke and reissue at any time.
Hosting
Self-hosted on a Raspberry Pi-class ARM box, run under PM2 from a checked-in ecosystem config with restart limits, sitting behind Cloudflare.
Email infrastructure
Cloudflare Email Routing feeding a TypeScript Cloudflare Email Worker, deployed with Wrangler, which parses MIME with postal-mime and extracts PDF text with unpdf before handing the parsed result back to the main server over an authenticated internal endpoint.
Storage
Flat files on disk. There is no database.
Alerting
Notable account and administrative events raise operational alerts, with per-event cooldowns so a burst never floods the channel.

The inbound email path is deliberately paranoid. The worker requires DMARC, or aligned SPF and DKIM, before it looks at anything else; then it gates on the sender's domain; then it confirms the sender has an active account. Permanent failures are rejected outright, while transient ones are re-thrown so delivery is retried, and every non-import outcome is written back into the app's shared audit log so a roster that quietly failed to arrive can be explained.

What it does now

A signed-in user drops a roster export into the browser, or emails it in, and sees a colour-coded preview of every shift with rule warnings before anything is written. Confirming pushes the whole cycle to their connected calendar. Re-importing a revised roster updates changed shifts, adds new ones and skips unchanged ones rather than creating duplicates.

Beyond import there is an Upcoming view with today's shift and what is next; an Equity view breaking the cycle into Day, Afternoon and Night counts and hours with compliance warnings, including breaches that only appear at the seam between two consecutive rosters; and a History view with one-tap undo for each import. A banner warns when the next roster is due, and escalates once the current one has ended with nothing newer imported.

Someone who uploads a whole-team roster covering everyone on the same schedule also sees which colleagues are starting at the same time, and can opt in to having their own shifts pushed automatically when a teammate uploads a newer copy. Behind that sits an in-app issue-reporting form and an admin console for user management, an audit log, broadcast messages and feedback triage.

The parsing is the fiddly part. Shift text is classified into ten types by pattern matching on the raw label; the CSV parser rebuilds quote-balanced logical lines before tokenising, so a shift cell split across two physical lines is still read correctly; and a user is matched to their own line on a whole-team roster by scoring their sign-in address against the names present, returning nothing at all when the match is ambiguous rather than guessing wrong.

Continued development

ShiftSync has been through dozens of releases in its first months, and is still shipping. Every one of them is a dated changelog entry, because every one was user-facing enough to warrant its own written note.

The changelog is dominated by corrections rather than feature announcements: a rest day silently dropped when a night shift began on the same calendar date, a paid day off miscounted as an extra night shift and inflating the equity totals, a crash for anyone using feed-subscription mode, and an accessibility pass covering auto-zoom on text fields, screen-reader labels, Escape-to-close on modals and a swipe-to-open mobile drawer. One release changed the default sign-in method to the 6-digit code because the emailed link was being blocked in transit for some people, a change that only reads as a response to users saying they could not get in.

The release process is codified rather than remembered. A validation script syntax-checks the source and refuses any deploy where the top changelog entry does not match the package version. The deploy script refuses to run off the release branch, refuses any version that is not strictly greater than the one already live, rebuilds the frontend, restarts the process manager, and then polls the live version endpoint until it confirms the new build is actually serving. Unseen changelog entries surface in the app once per account and are then marked seen server-side, so the same update never reappears on another device.

That is the part worth pointing at. Shipping the first version is the easy half; dozens of releases after it, most of them fixing something that only showed up in real use, is what maintaining software actually looks like.

ShiftSync is not linked from this page. Access is restricted to verified members of the organisation it was built for, so a public link would lead nowhere useful.

ShiftSync is an independent product built and owned by PiStack. It is not affiliated with, endorsed by, sponsored by, or contracted to any government organisation or agency.

Get in touch