How it works · StaffPortal

Five modules, seven commands, one afternoon.

How an MIT-licensed staff platform deploys in under thirty minutes, on infrastructure you control, with no per-seat fees.

The 60-second version

If you only read one paragraph.

StaffPortal is a Next.js application targeting Supabase + Vercel. Schema lives as numbered SQL migrations applied with the Supabase CLI. Three roles (owner, manager, employee) are enforced as row-level security policies in Postgres, not as application code. Five modules ship out of the box: auth, leave, attendance, payslips, and a folder-based document store. Deploy is seven commands documented in the README. Branding lives in one config file. No Docker, no telemetry, MIT licensed.

The deploy story

From clone to live, in seven commands.

01
git clone https://github.com/<org>/staffportal && cd staffportalClone the repo
02
pnpm installInstall dependencies
03
supabase link --project-ref <your-ref>Link to your Supabase project
04
supabase db pushApply schema migrations and RLS policies
05
cp .env.example .env.local && $EDITOR .env.localSet Supabase URL, anon key, service role key, branding
06
vercel --prodDeploy to Vercel (or any Next-compatible host)
07
pnpm exec staffportal invite <you@company.com> --role ownerInvite yourself as owner; check email; sign in

Anyone who has shipped a Next.js + Supabase app before will be live in under thirty minutes. The README walks through each step with screenshots. The hardest part is usually choosing a Supabase region.

Core data flow

A leave request, end to end.

┌──────────────────────────────────────────────────────────────┐ │ Employee opens /leave/new │ │ → Server component reads leave_balances (RLS-scoped) │ │ → Client form pre-fills remaining days │ │ Employee submits the request │ │ → Server Action: INSERT INTO leave_requests (status=open) │ │ → Trigger: write event to leave_events audit table │ │ → Resend email to manager (React Email template) │ │ • inline summary, single-tap approve URL │ │ Manager opens approve URL │ │ → Auth via signed token + cookie │ │ → Server Action: UPDATE status = 'approved' (RLS gate) │ │ → Trigger: deduct balance, write audit event │ │ → Resend email to employee + cc'd HR if configured │ │ Done. Calendar export endpoint reflects the approved leave. │ └──────────────────────────────────────────────────────────────┘
Subsystems

The five modules.

Auth + profiles

Invite-only. Owner creates invitations from the admin module; the invitee receives a magic-link email; on first sign-in they pick a name and password (or stay magic-link only). Role assignment lives on profiles.role; manager-of relationships live on profiles.manager_id.

Leave

UK statutory defaults: 28 days including 8 bank holidays. Bank holidays auto-deducted per region (config). Leave types: annual, sick, parental, unpaid. Balances accrue monthly via a pg_cron job; year-end reset is a one-line SQL admin action.

-- Monthly accrual job INSERT INTO leave_balances (user_id, leave_type, days, year) SELECT p.user_id, 'annual', 28.0/12, EXTRACT(YEAR FROM now()) FROM profiles p WHERE p.active ON CONFLICT (user_id, leave_type, year) DO UPDATE SET days = leave_balances.days + EXCLUDED.days;

Attendance

Single-button clock-in / clock-out, with an optional lunch toggle. Punches accumulate in attendance_punches; a derived view computes daily worked-minutes per user. Optional kiosk mode is a separate route, intended for tablet-on-wall deployments.

Payslips

Admin uploads PDFs to /payslips/{user_id}/{period}.pdf in Supabase Storage; metadata lands in the payslips table. Employees see only their own; downloads happen via short-lived signed URLs. Bulk upload supports a CSV mapping employee_id → file.

Documents

Folder tree with role-scoped read access. The documents table stores folder + file metadata; binaries go to Storage. RLS on the table controls who can see which folders. A typical setup: /policies (everyone), /managers (managers + owners), /owners (owners only), /personal/{user_id} (the user).

Stack and reasoning

Why this, not that.

Next.js (App Router)

Why we picked it

Single full-stack framework. Server components for read paths, server actions for mutations. One repo, one deploy, no separate API service.

What we rejected

Remix is fine but the Vercel-native deploy path on Next is one click. For a template aimed at non-engineer founders, that matters.

Supabase (Postgres + Auth + Storage)

Why we picked it

One vendor for everything. RLS, magic-link auth, signed-URL storage, all on a generous free tier. The schema is portable Postgres if you ever want to leave.

What we rejected

Hand-rolled Postgres on a VPS plus NextAuth plus S3 plus a websocket server is four things to operate. The whole point is to not need an ops team.

Schema as Supabase migrations

Why we picked it

Every table, policy, trigger lives in numbered SQL files. Forks can diff cleanly. Reproducible, version-controlled, applied with one CLI command.

What we rejected

Click-ops in the Supabase dashboard is unreproducible and lossy. Migrations are the only sustainable answer.

Tailwind + shadcn/ui

Why we picked it

Defaults that look professional, components you can read, no theme system to fight. shadcn copies code into your repo so forks own their UI.

What we rejected

Material UI is heavy, opinionated, and hard to fork-customise without painful overrides.

React Email + Resend (optional)

Why we picked it

Templates as React components, edit them like any other JSX. Resend is generous on free tier; switch to Postmark or SES with one env var change.

What we rejected

Hard-coded HTML strings rot. SMTP-only is operationally hostile.

Vercel deploy target

Why we picked it

One-click deploy from GitHub. Branch previews. Free tier easily covers 50+ staff. The app is a vanilla Next.js project so it runs on any Next host.

What we rejected

Self-hosting on Fly or Railway is fine; the docs cover it. Default is Vercel because it has the lowest activation energy.

Contributor workflow

How forks and PRs work.

Local development

supabase start brings up a local Postgres + Auth stack. pnpm dev runs Next.js. supabase db reset reapplies migrations from scratch with seed data.

CI checks

GitHub Actions runs lint (Biome), typecheck (tsc), tests (Vitest), and a production build on every PR. Required to pass before merge.

Schema changes

New migrations are appended; never edit historical migrations. RLS changes go in the same migration as the schema change they protect.

Module additions

New modules live under app/(module-name) with their own migrations, server actions, and email templates. Core stays small; modules can be opt-in via config/modules.ts.

Operating posture

What it costs to run.

£0
Supabase + Vercel free tiers cover ~50 staff comfortably
~£25
Per month at 100+ staff (Supabase Pro + Vercel Pro)
0
Per-seat licence fees, ever
Roadmap

What is next.

Expenses module (opt-in)

Photo-capture + structured-extraction pipeline, optionally enabled. Same approval pattern as leave.

Timesheets

Project-based time tracking for back-office staff. Joins the existing payroll-export pattern.

Internal directory

Searchable photo + role + manager view. Already half-built into profiles.

Asset register

Track issued laptops, phones, keys per employee. Same UI conventions, separate module.

Want the engineering record?

The whitepaper covers the architecture, the design philosophy, and the lessons learned in detail.