A staff portal small businesses can actually self-host.
Engineering record for StaffPortal: an MIT-licensed reference implementation of an internal staff platform. Five modules out of the box, deployable in an afternoon on Vercel + Supabase, no Docker, no per-seat fees.
Executive summary
StaffPortal is the open-source distillation of patterns I rebuild on every internal-platform engagement. It ships as an MIT-licensed Next.js application targeting Supabase + Vercel, with five working modules (auth, leave, attendance, payslips, documents), pre-baked RLS policies for owner / manager / employee roles, schema as Supabase migrations, and a deploy story documented as seven literal commands. The core 80 percent that every small business needs, free, forkable, and live in an afternoon.
01Background
Small businesses (20 to 100 staff) sit in the worst part of the HR-software market. Too small for proper HRIS platforms, which charge enterprise pricing. Too big for a shared spreadsheet. The middle ground is generic SaaS at £6 to £10 per seat per month, none of which quite fits how a real small business operates.
I have built variants of the same internal platform multiple times for clients. Every time, the core 80 percent is identical: auth, role-based access, leave, attendance, payslips, a place to keep policy documents. The other 20 percent is genuinely client-specific. Open-sourcing the core 80 percent felt obvious.
02The problem in detail
The market gap
- Enterprise HRIS (Workday, Personio, BambooHR mid-tier): £8 to £15 per seat per month, plus implementation fees. Painful at 50 staff.
- Cheap SaaS leave trackers: cheap per-seat but generic — assumptions about company structure that do not match the team.
- Existing open-source HR projects: either dead, or bloated with a kitchen-sink feature set that requires Docker, Redis, and a full ops team to run.
- Spreadsheets: the default. Painful at scale, no audit, no role separation.
The technical gap
A small business owner who is technical-adjacent — the kind of person who can run git clone and follow a README — should be able to deploy a real internal platform without writing a cheque, signing a contract, or learning Kubernetes. That is the audience StaffPortal is built for.
03Goals and non-goals
In scope
- Deploy in under 30 minutes by someone who can read a README
- Five working modules out of the box: auth, leave, attendance, payslips, documents
- Pre-baked RLS policies for owner, manager, employee
- UK statutory leave defaults plus configurable bank holidays
- Branding via single config file (logo, colours, company name)
- Email templates as editable React Email components
- MIT license, zero telemetry, zero phone-home
Explicitly out of scope
- Performance reviews, recruiting, ATS. Domain-specific; better as forks.
- Native mobile app. Responsive web; PWA install if you want home-screen access.
- Multi-tenancy. One company per Supabase project. Cleaner mental model, simpler RLS, better data isolation.
- Self-hosting Postgres yourself. Supabase managed, or fork the schema and run it wherever.
- Docker. Deliberately no Dockerfile. The goal is one-command deploy on Vercel.
04Architecture
┌─────────────────────────────────────────────────────────────┐
│ Browser (employee / manager / owner) │
│ ↓ Supabase Auth cookie │
│ Vercel → Next.js (App Router) │
│ ↓ supabase-js with RLS-aware queries │
│ Postgres on Supabase │
│ ├── profiles, roles, invitations │
│ ├── leave_types, leave_requests, leave_balances │
│ ├── attendance_punches │
│ ├── payslips (Storage-backed metadata) │
│ └── documents (folder-tree, role-scoped) │
│ │
│ Supabase Storage │
│ ├── /payslips/{user_id}/... │
│ └── /documents/{folder}/... │
│ │
│ Resend (optional) — invitation, leave-decision, weekly │
│ │
│ GitHub Actions: lint, typecheck, test, build, deploy preview│
└─────────────────────────────────────────────────────────────┘Repo layout
staffportal/ ├── app/ # Next.js App Router pages │ ├── (auth)/ # sign-in, accept-invite │ ├── (employee)/ # leave, attendance, my-payslips │ ├── (manager)/ # team views, approvals │ └── (admin)/ # invitations, settings, exports ├── lib/ │ ├── auth.ts # supabase server client, role guards │ ├── leave.ts # accrual, balance, holiday calendars │ ├── attendance.ts # punch logic, derived hours │ └── email.ts # React Email + Resend helpers ├── supabase/ │ ├── migrations/ # SQL, applied via Supabase CLI │ └── seed.sql # demo data for local dev ├── config/ │ └── branding.ts # name, logo path, colours └── README.md # the deploy story
05Key technical decisions
Conventions over configuration
Every choice that can be a default is a default. UK statutory leave (28 days including 8 bank holidays) is the default. Three roles is the default. Folder-based document permissions are the default. The schema is shipped, not configured. A small business deploying StaffPortal should never have to make a schema decision; they should change a number in config/branding.ts and ship.
One stack, no exits
Next.js, Supabase, Vercel, Tailwind, shadcn/ui, React Email, Resend. No Docker, no Redis, no microservices, no message queue, no custom infra. The complete operational surface is two dashboards (Supabase + Vercel) and one email vendor. A future maintainer can be productive in an afternoon.
Schema as migrations, not GUI clicks
Every table, column, policy, and trigger lives in a numbered SQL migration file under supabase/migrations/. Applied with supabase db push. Forks can diff their schema against upstream cleanly. There is no “please click these things in the dashboard” step — that path is unreproducible and breaks at scale.
RLS pre-baked
Three roles, ten tables, with RLS policies shipped in the migrations. A fork inherits the security model for free; modifying it is editing one SQL file. Example:
-- Employees see only their own leave requests.
-- Managers see their direct reports'.
-- Owners see everything.
CREATE POLICY "leave_select" ON leave_requests FOR SELECT
USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM profiles p
WHERE p.user_id = leave_requests.user_id
AND p.manager_id = auth.uid()
)
OR EXISTS (
SELECT 1 FROM profiles p
WHERE p.user_id = auth.uid() AND p.role = 'owner'
)
);Branding as config, not theme system
Two months of skinning hell can be avoided by exposing exactly what most forks need: company name, logo path, primary colour, support email. Anything more complex is a code change. Most forks need only branding.ts.
Zero telemetry, zero phone-home
StaffPortal does not call any Sarma-Linux endpoint. There is no analytics beacon, no “check for updates” ping, no usage report. A self-hostable HR tool that phones home is a non-starter; the whole point is data sovereignty.
06Implementation milestones
| Phase | Deliverable |
|---|---|
| 0.1 | Auth + profiles + roles. Invite-only sign-in. Three roles wired into RLS. |
| 0.2 | Leave module: types, balances, requests, approvals, UK statutory defaults. |
| 0.3 | Attendance punches, daily worked-minutes view, manager weekly summary. |
| 0.4 | Payslip upload (admin), employee-only download via signed URLs. |
| 0.5 | Document store: folder tree, role-scoped folders, drag-and-drop upload. |
| 0.6 | README polish, screenshots, seven-command deploy story, contributor guide. |
| 1.0 | Public release on GitHub. MIT licensed. Issue templates + PR template. |
07Results
StaffPortal is now the foundation of every internal-platform engagement I take on. A new client project starts as a fork, branded, with the client-specific 20 percent layered on top. Project lead time has roughly halved. Quality has gone up because the core has been hardened across multiple deployments.
For self-hosting users, the value is straightforward: data sovereignty, zero per-seat fees, generous free tiers covering meaningful scale (50+ staff comfortably on Vercel + Supabase free tiers). Several forks have submitted PRs back upstream — bug fixes, accessibility improvements, a German translation, a regional bank-holiday seed for Ireland.
08Lessons learned
Defaults are the product
For a self-hostable template, the defaults are everything. UK statutory leave, sensible role names, reasonable email copy. The configurability matters much less than the defaults. Most forks change three things and ship.
Documentation is part of the deploy
The README is engineering work, not an afterthought. I spent as long on it as on some modules. If the deploy story breaks, the project dies, no matter how good the code is.
Boring stacks are kind to future maintainers
Next.js, Supabase, Tailwind, shadcn. Nothing requires reading a 200-page book to contribute. Boring is a feature for open-source software.
Small surface area beats big feature set
Five modules. The temptation was to ship ten. Five is enough to be useful, small enough to maintain, and leaves room for forks to add domain-specific modules without fighting core.
Open source is a multiplier on client work
Every paid client engagement now sits on top of StaffPortal. The hours invested in the open-source codebase pay back many times over in faster, more reliable client deliveries.
09Conclusion
StaffPortal is a deliberate statement about how to think about open source on top of client work. The interesting parts are the patterns, not the specific implementations. Open-sourcing the patterns gives them a longer life than any single project, lets smaller businesses access tools that used to be the preserve of bigger budgets, and makes every paid project I take on faster and more reliable. The trade is favourable in every direction.