What it does
I built ShiftView to solve a problem I faced as a retail manager: shift scheduling and timekeeping. Someone calls out, two people swap a shift in a group text, and by mid-morning nobody is sure who is on the floor or whose hours are right. ShiftView handles that whole loop in one app, for managers and for the people on the floor.
Scheduling
- Week and month views, shift templates and copy-week
- A Draft mode to plan the next week privately, then publish it in one step
- Auto-schedule: drafts the week from coverage targets, availability, time off and hour limits, and explains any gap
- Availability and time-off conflicts checked on every edit
Changes during the day
- Call-outs in one tap, with managers notified instantly
- Shift swaps in two steps: the coworker accepts, then a manager approves
- Open shifts that eligible staff can claim
- Time-off requests and punch corrections with approval
Time clock and coverage
- Clock in and out, with an optional geofence checked on the server
- A live timeline of target, scheduled and clocked-in headcount
- Late clock-in and low-coverage alerts
- Time cards, payroll-ready CSV exports and labor-cost reports
Team and platform
- Direct messages, encrypted at rest, and announcements
- Web push notifications with per-person preferences
- One account across several stores
- Installable on phones, tablets and desktops, with no app store
Anyone can try it without signing up. The demo button signs you in to a seeded store as a manager, with full read and write access, and a nightly job resets it.


How it’s built
A Next.js app on Supabase, with 65 API route handlers as the only way to change data. The scheduling engine is a separate pure function with no database or network access, so it can be tested and benchmarked on its own.
- Front end
- Next.js 16 App Router, React 19, TypeScript (strict), Tailwind CSS v4
- Data and auth
- Supabase: Postgres with row-level security, email sign-in codes, anonymous demo sessions
- Live updates
- Supabase Realtime for schedules and messages; Web Push (VAPID) for notifications
- Scheduling engine
- Pure TypeScript in lib/scheduler: greedy construction, then simulated annealing
- Email and bots
- Resend for alert email; Cloudflare Turnstile on sign-up, the demo and the contact form
- Testing and CI
- Vitest, Testing Library, Playwright, SQL suites; GitHub Actions CI; Vercel previews, production and cron


Auth and security
Security is layered so that no single check is the only thing standing between a user and another store’s data.
- Passwordless sign-in. People sign in with one-time email codes, so there are no passwords to store or leak. Invites link an account to an employee record on first sign-in.
- Sessions verified locally. Pages check the session JWT with
getClaims(), which can verify it locally with asymmetric signing keys instead of a round trip to the auth server. A Next.js proxy refreshes sessions for pages, while API routes authenticate themselves and answer 401 or 403 instead of redirecting. - One write path, checked twice. Route handlers are the only way to change data. Each checks the session and role, validates input and writes an audit log entry. Row-level security in Postgres enforces the same tenant and role rules again, so a request that skips the API can’t do more than the API allows.
- Tenant isolation in the schema. The organization comes from the caller’s own memberships on the server, never from the request. Composite foreign keys make a cross-store reference a constraint violation.
- Least privilege in the database. Privileged functions check the caller themselves or can only run with the service role. Column grants hide pay rates from the API, and managers read them through one checked function.
- Tamper-resistant time clock. Punches are stamped with the database clock, and the geofence is enforced on the server and in the database, not only in the browser.
- Data and abuse protection. Messages are encrypted at rest with AES-256-GCM. Cloudflare Turnstile guards sign-up, the demo and the contact form, and responses carry standard security headers.
CI/CD and testing
Every push to main or dev and every pull request runs five GitHub Actions jobs in parallel:
- static-checks
- ESLint and a strict TypeScript typecheck
- unit
- Vitest and Testing Library: route handlers against mocked Supabase clients, components, the scheduling engine
- unit-timezones
- The same suite again at UTC+14 and in a half-hour, daylight-saving zone, because date bugs hide at midnight
- security
- 19 SQL suites on a fresh Postgres 17 for row-level security, cross-store isolation, function privileges and punch integrity, plus the security unit tests
- e2e
- Playwright on a production build, across phone, tablet, tablet landscape, desktop and a far-timezone phone
The Playwright specs cover the parts most likely to break quietly: auto-scheduling, the week planner, the demo, color contrast, layout shift, and a store and phone on opposite sides of the date line. A failed run uploads the Playwright report so the failure can be replayed.
Vercel deploys every branch to a preview URL and main to production. A production build fails if any required secret is missing or malformed, so a bad config keeps the last good version live. Two cron jobs send shift reminders and reset the demo store each night.
Key decisions
An optimizer, not a chatbot, for auto-scheduling
A language model can’t guarantee hard rules. It might schedule someone on approved time off, and it can’t prove a gap was unavoidable. Rostering is a constraint problem, so I wrote a constraint optimizer. It builds the week greedily, neediest 15-minute slot first, then improves it with simulated annealing against a weighted penalty score.
Hard rules like availability, rest between shifts and the overtime line are never broken. The random choices come from a seeded generator stored on each run, so the same input always gives the same schedule. Every gap it can’t fill comes with the reason and a one-tap fix. The engine has a narrow interface, so an exact solver could replace it later without touching the API or the UI.
Multi-tenant without downtime
ShiftView started as a single-store app. I compared a shared schema, a schema per tenant and a database per tenant. I chose a shared schema with an org_id on every row: it works with Supabase’s auth and realtime out of the box, and row-level security still gives isolation the database enforces.
Composite foreign keys make a cross-organization reference a constraint violation, so even a buggy admin write can’t point a shift in one store at an employee in another. The migration ran as expand, backfill, enforce, contract, so the old code kept working until the new code was live.
Row-level security that stays fast
The first policies called a membership function once per row. I rewrote them to compare each row with the caller’s organizations, looked up once per query. On 50,000 shifts that was 12–35× faster.
Times as minutes, and “off” derived, not stored
A shift is stored as minutes since midnight (480 is 8:00 AM) and belongs to the day it starts, so an overnight shift just runs past 1440. Dates are plain strings in the store’s timezone, so a phone’s timezone never decides what “today” is.
Nobody has an “off” row. A person is off when they have no shift that day, which leaves one source of truth to keep in sync instead of two.
What I’d change
Track migrations from day one. I apply migrations in the SQL editor, so Supabase keeps no history and two files share a version number. A checked-in baseline and a schema fingerprint check keep production honest, but a migration tool from the start would have been simpler.
Faster first visits on slow phones. A performance pass cut the JavaScript and the waterfalls, but a cold load on a throttled phone is still slower than I want. The next steps are splitting the shared stylesheet and trimming the JavaScript every page loads.
An exact solver, if stores need it. The heuristic fills every week I’ve benchmarked, but it can’t prove a schedule is optimal. CP-SAT behind the same interface would.

