ShiftView

Scheduling, a time clock and live coverage for retail and fulfillment teams. I designed and built it alone, from the data model to the installable app.

Solo · Product, design and engineering · Public beta · Next.js 16, TypeScript, Supabase · Live ↗ · Code ↗

ShiftView home page: an employee clocks in ten minutes late and the store manager’s dashboard shows the Late Clock-In alert.

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.

ShiftView week view after auto-scheduling 46 shifts, showing full coverage, hours against the budget and zero overtime.
Draft mode after an auto-schedule run: the summary card, the week grid and each gap it could or couldn’t fill.

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.

ShiftView architectureThe browser sends every write through Next.js route handlers, which check auth, validate input and write an audit log before reaching Postgres, where row-level security checks the same rules again. Supabase Realtime pushes changes back to the browser. Route handlers also send web push notifications and email.BrowserReact 19 · installable PWARoute handlersauth · validate · audit logPostgresRLS on every tableAuto-schedule enginepure TypeScript, no I/ORealtimeschedules, messagesWeb Push · Resendnotifications, emailwriteslive updatespush
Every write goes through a route handler. Row-level security in Postgres enforces the same rules a second time.
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
ShiftView on a laptop, a tablet and a phone, showing the same store’s schedule.
One codebase, installed as a web app on every device a store has.

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.