ShiftPilotUser Manual
Back to app

Getting Started

ShiftPilot is a real-time workforce scheduling platform for clinics — staff scheduling, credential-gated assignment, coverage assurance, on-call management, time tracking, and full audit history, with self-service tools for staff and oversight tools for admins.

This page covers what the system is made of, how to run it locally, how it's deployed, and how login/roles work. Every other page in this manual assumes you've read this one.

The five principles behind the design

  1. Real-time and synchronized. Any change (a swap approval, a call-off, an availability edit) is reflected immediately everywhere it matters — the schedule grid, personal calendars, the dashboard.
  2. Eligibility is enforced, not assumed. A person can only be scheduled into a role they're credentialed and competent for. Overriding that is possible but never silent — it's tracked and alerted.
  3. The ideal is always visible. Every station/service carries a target staffing level, and the gap between target and actual is a first-class, always-visible thing — not something you discover later.
  4. Everything is attributable. Every meaningful mutation records who, what, when, before-value, after-value, and (where relevant) why.
  5. Web + mobile parity for review. Building schedules is desktop-first; reviewing, approving, and auditing work equally well on a phone.

How the pieces fit together

Two engines sit underneath almost everything in the product, and it's worth understanding them before anything else:

  • The eligibility engine (src/lib/eligibility.ts) answers one question: is this staff member allowed to be placed into this role, at this station, on this date? It checks three things — active, non-expired competencies matching what the role requires; the staff member's role type matching what's allowed; and the station being one of their qualified locations. Every module that assigns people to something (the schedule grid, on-call, swaps) calls into this same engine, so eligibility answers never drift between modules.
  • The coverage engine (src/lib/coverage.ts) answers the second question: for this station/service/time window, how does the actual assigned headcount compare to the staffing model's target and minimum? This powers the shortage banner on the schedule grid, the dashboard's coverage tiles, and the clinic-snapshot email.

Everything else builds on top of those two answers, and is tied together by two more cross-cutting pieces:

  • The audit trail (src/lib/audit.ts) — every mutation across every module writes one audit entry through a single shared function, so the audit log is complete by construction rather than by every module remembering to log itself.
  • The notification pipeline (src/lib/notify.ts) — the same events that get audited can also fan out to anyone subscribed to that event type/scope, across email, in-app, and (simulated, see below) push/SMS.

See Scheduling Grid & Coverage and Staff, Credentials & Organization for how these two engines actually show up in the UI.

Stack

  • Next.js 16 (App Router, Turbopack), React 19, TypeScript
  • Prisma 7 with the @prisma/adapter-mariadb driver adapter, against MySQL 8
  • Tailwind CSS 4 + shadcn/ui components
  • jose (JWT sessions) + bcryptjs (password hashing) for auth
  • @dnd-kit for the drag-and-drop schedule grid
  • recharts for report charts
  • ics / node-ical for calendar export/import
  • nodemailer for email (falls back to a simulated/logged send when no SMTP is configured — see Email, Calendar & API)

Running it locally

git clone https://github.com/wickedfool/shiftpilot.git
cd shiftpilot
docker compose up -d          # starts a dedicated MySQL 8 container on localhost:3307
npm install
npx prisma migrate dev        # applies the full schema
npx prisma db seed            # seeds a full sample clinic (see below)
npm run dev                   # http://localhost:3005

docker-compose.yml runs MySQL in its own container so it's isolated from anything else on your machine and survives a reboot (the whole point of using a real database instead of something in-memory). The .env file holds DATABASE_URL and JWT_SECRET — copy .env.example if you're starting fresh.

The seed data

prisma/seed.ts builds a complete fictional clinic — Sunrise Occupational Health Clinic — from scratch every time it runs (it resets the database first, so it's safe to re-run whenever you want a clean slate):

  • Full org hierarchy: one facility, two departments, eight stations, eight services
  • A role taxonomy across all five role groups, an eleven-item competency catalog
  • 63 staff members with realistic competency coverage — including deliberate gaps and a few expired/expiring credentials, so the credential-gating features have something real to enforce
  • A staffing model with ten coverage requirements
  • Six weeks of assignments (one past, one current, four forward) with intentional shortages, overstaffed windows, and a few forced competency-override exceptions
  • On-call rotations (including a deliberate coverage gap), availability records, time-off requests, swap requests, time punches, attendance events, alerts, audit entries, and notifications

Because the demo/dev-login accounts (see below) would otherwise have an empty personal schedule, the seed script also gives the Provider/Staff, Charge/Lead, and On-Call Coordinator dev accounts a real past-and-upcoming schedule so My Schedule has something to show immediately.

Signing in

ShiftPilot login page with developer quick-login grid

The login page has two ways in:

  1. Email + password, for real accounts.
  2. Developer quick login — one click per role, instantly signs you in as that role's seeded demo account. This only works when NODE_ENV !== "production", or when ALLOW_DEV_LOGIN=true is set explicitly — so it's safe to leave the code in place without worrying about it being reachable on a real deployment unless someone deliberately turns it on.

The eight roles

Permissions are role-based; every account has exactly one systemRole. The dev-login password for every seeded account is ShiftPilot123!.

RoleWhat they can do
System AdminFull platform configuration
Enterprise AdminCross-facility oversight, org hierarchy, credential catalog
Facility AdminSite-level management, staffing models, approve overrides
SchedulerBuild/edit schedules, assign staff, approve requests & swaps
Charge / LeadDay-of adjustments — call-offs, reopening shifts
Provider / StaffPersonal schedule, availability, time-off, swaps, clock in/out
On-Call CoordinatorBuild/publish on-call, escalation chains, on-call swaps
AuditorRead-only — schedules, audit log, reports

Two permissions are granted independently of role and tracked separately: canOverrideCompetency (force an assignment past a missing credential — always audited) and canOverrideOvertime (approve a schedule that pushes someone past the overtime threshold).

Capabilities — fine-grained grants on top of roles

Some newer, sensitive features aren't gated by role at all but by capabilities: named, per-user (and optionally per-facility) grants managed at Access & Scopes. This lets you, say, let a supervisor view compensation without changing their role, or grant one person the ability to override leave limits at a single facility. Capabilities in use today include VIEW/EDIT_COMPENSATION, VIEW_LABOR_COST_REPORTS, MANAGE_LABOR_RULES, MANAGE_ACCRUAL_POLICIES, VIEW/MANAGE_PROTECTED_LEAVE, MANAGE_LEAVE_RULES, and OVERRIDE_LEAVE_LIMITS. Effective access = role default ± per-user grants/revocations, resolved fresh on every request. See Compensation & Labor Rules and Leave Governance.

Command-center dashboard for a Scheduler

Deployment

The production instance runs on a shared Hetzner VPS behind nginx + Let's Encrypt, managed by PM2, with DNS on Cloudflare (shiftpilot.builtincase.com). The pattern:

# on the server, inside /opt/shiftpilot
git pull
npm install
npx prisma generate
npx prisma migrate deploy     # never `migrate dev` against production
npm run build
pm2 restart shiftpilot --update-env
  • MySQL runs in its own Docker container on the server (shiftpilot-mysql), bound to 127.0.0.1:3306 only — never exposed externally.
  • nginx reverse-proxies shiftpilot.builtincase.com to the app's local port, with a shared multi-domain Let's Encrypt certificate covering it alongside the server's other sites.
  • The weekly email digest cron (see Email, Calendar & API) is a system crontab entry that hits a secret-protected endpoint hourly — not a long-running process, so it survives app restarts without any extra supervision.
  • ALLOW_DEV_LOGIN is left enabled in this particular deployment so the team can demo/test every role instantly; turn it off (ALLOW_DEV_LOGIN=false or just running with NODE_ENV=production and no override) for any deployment where that shouldn't be publicly reachable.

Where to go next