Skip to content
sarmakska edited this page Jun 1, 2026 · 5 revisions

StaffPortal

A complete, self-hosted, open-source staff management platform built with Next.js, Supabase, and Tailwind CSS.

StaffPortal gives a small-to-medium organisation a single role-based portal for attendance, leave, timesheets, expenses, visitors, wellness, and IT support, with single sign-on, an immutable audit trail, GDPR data exports, monthly leave accruals, a mobile-friendly kiosk, and an optional conversational AI assistant. You run it on your own Supabase project and Vercel deployment, so staff data never leaves infrastructure you control. This page is the deep reference. For a fast install, jump to Quick-Start; for the module-level breakdown, see Architecture; for what is planned, see Roadmap.

The newest capabilities each have a dedicated page: Single-Sign-On, Leave-Accruals, GDPR-Export, Kiosk-Mode, and Audit-Log. Per-area deep dives live at Attendance, Leave, Kiosk, Visitors, Crons, Reception, Diary, and Exports.

What it does

Area Capability
Attendance Clock in/out, work-from-home toggle, late-arrival detection, correction requests
Timesheets Weekly hours against contracted hours, Excel export
Leave Annual, sick, maternity/paternity, unpaid; multi-step approval; PDF certificates; withdrawal with balance reversal
Expenses Claims with receipt upload, category and merchant, PDF claim forms
Purchase requests Submit and route to admin for approval
Visitors Pre-registration with QR references, host email on check-in, PDF passes, reception desk, public kiosk
Wellness Mood check-ins, breathing exercises, stretch reminders, admin dashboard
IT support Ticket submission and tracking, auto-cleanup of resolved tickets via cron
Admin Users, departments, schedules, leave allowances, leave accruals, corrections, audit log, analytics with CSV export, bank reconciliation
Single sign-on Route staff to Microsoft Entra ID, Google Workspace, GitHub, GitLab, or SAML 2.0 by email domain
Leave accruals Monthly accrual top-ups per balance, capped at full entitlement, run by cron or on demand
GDPR export One-click portable JSON export of every record held about a member
AI assistant (the assistant) Optional conversational assistant aware of your attendance, leave, expenses, and team

Architecture in depth

StaffPortal is a single Next.js App Router application. There is no separate backend service: data access happens through React Server Components and server actions that talk to Supabase directly, and a small set of API route handlers cover the AI assistant and scheduled jobs.

flowchart TD
    subgraph Client
        Browser["Staff browser"]
        Kiosk["Public kiosk (/kiosk)"]
    end
    subgraph App["Next.js on Vercel"]
        MW["middleware.ts: session refresh + role guard"]
        RSC["Server components + pages"]
        Actions["Server actions (lib/actions)"]
        ChatAPI["/api/chat (the assistant)"]
        CronAPI["/api/cron/* handlers"]
    end
    subgraph Supabase
        Auth["Supabase Auth + SSO"]
        DB[("PostgreSQL + Row Level Security")]
        Storage["Storage (receipts, assets)"]
    end
    Resend["Resend (transactional email)"]
    Groq["Groq API (optional LLM)"]
    IdP["Identity provider (Entra ID / Google / SAML)"]
    Scheduler["Vercel Cron / GitHub Actions"]

    Browser --> MW --> RSC
    Kiosk --> RSC
    RSC --> Actions --> DB
    Actions --> Auth
    Auth --> IdP
    Actions --> Storage
    ChatAPI --> Groq
    ChatAPI --> DB
    CronAPI --> DB
    CronAPI --> Resend
    Scheduler -->|Bearer CRON_SECRET| CronAPI
Loading

Request and auth flow

  1. Every request passes through middleware.ts, which refreshes the Supabase session cookie via @supabase/ssr and redirects unauthenticated users to the login page.
  2. Authenticated pages render as server components. They read data through server actions in lib/actions, which use a request-scoped Supabase client carrying the user's session.
  3. Authorisation is enforced twice: the middleware gates routes by role, and Postgres Row Level Security policies filter every row by the authenticated user and their role. The service-role key is used only in trusted server contexts (cron handlers, admin operations) and is never shipped to the client.
  4. The public kiosk at /kiosk is the one route that bypasses login. It uses scoped server actions for clock in/out and visitor check-in only, authenticated with a per-user kiosk PIN.
  5. Single sign-on is layered on Supabase Auth. When a member enters an email whose domain has an active connection in sso_connections, the login screen calls signInWithOAuth (Entra ID, Google, GitHub, GitLab) or signInWithSSO (SAML 2.0) and redirects to the identity provider. The OAuth or SAML response returns through /auth/callback, where a first-time SSO user is bootstrapped with a profile and standard leave balances and the login is audited. See Single-Sign-On.

Roles

Role Access
employee Own attendance, timesheets, leave, expenses, diary, calendar, announcements
reception Employee access plus visitors, reception desk, kiosk settings
director Employee access plus analytics, all timesheets (read-only), staff summary
accounts Employee access plus all timesheets (read-only), expense reports
admin Full access

Scheduled jobs

Cron endpoints under /api/cron/* are authenticated with an Authorization: Bearer <CRON_SECRET> header so only your scheduler can trigger them. Drive them with Vercel Cron (vercel.json), the bundled GitHub Actions workflows, or any external scheduler.

Job Endpoint Suggested schedule
Birthday reminders /api/cron/birthday-reminder Daily 08:00
Absent reminders /api/cron/absent-reminder Daily 10:00
Missing attendance /api/cron/missing-attendance Daily 18:00
Forgotten clock-out /api/cron/forgotten-clockout Daily 20:00
Stretch reminder /api/cron/stretch-reminder Weekdays 14:00
IT ticket cleanup /api/cron/it-ticket-cleanup Weekly
Diary reminders /api/cron/diary-reminders Daily
Leave accrual /api/cron/leave-accrual Monthly, 1st at 00:10 UTC
Year-end rollover /api/cron/year-end-rollover Yearly, 1 Jan at 00:05 UTC

Real-world examples

Onboarding a new employee

An admin creates the account under Admin then User Management, assigns the employee role and a department, and sets contracted hours and working days under Work Schedule Management. The new starter signs up with the seeded email domain, verifies their address, and lands on a personalised dashboard showing leave balances and the week's hours. Their leave allowance is set under Leave Allowances, including any carry-forward cap.

Submitting and approving an expense claim

An employee opens Expenses, enters the merchant, category, and amount, and uploads a receipt photo, which is stored in Supabase Storage. A Resend email notifies the approver. The approver reviews and approves in the admin UI, and a PDF claim form is generated on demand by PDFKit. The claim then appears in the accounts export for bank-statement reconciliation.

Front-desk visitor check-in

Reception pre-registers a visitor, which generates a QR reference. On arrival the visitor is checked in at the reception desk or the public kiosk, the host receives an email notification, and a printable PDF visitor pass is produced. Resolved records age out automatically.

Accruing leave month by month

An accounts user sets a balance's monthly accrual rate, for example two days of annual leave per month towards a twenty-four-day entitlement. On the first of each month the leave-accrual cron tops the balance up by the months that have elapsed since it last ran, never exceeding the entitlement. The job is idempotent, so a manual re-run the same day grants nothing further. The preview under Admin, Leave Accruals shows exactly what the next run will grant. See Leave-Accruals.

Exporting your own data

A member opens Settings, Privacy and data, and selects Download my data. The app gathers their profile, attendance, leave, expenses, diary, visitors, feedback, complaints, and the audit events attributed to them into a single JSON document and streams it back as a download. The export is recorded in the audit log. An administrator can export another member by adding ?userId=<id> to the endpoint. See GDPR-Export.

Configuration

All configuration is via environment variables. Copy .env.local.example to .env.local and fill in real values. The required set is the Supabase URL and keys, the Resend API key and from-address, the public app URL, and CRON_SECRET. The Groq API key and notification routing addresses are optional. Never commit .env.local or expose SUPABASE_SERVICE_ROLE_KEY or CRON_SECRET to the client.

Troubleshooting

Build fails locally with missing environment variables. The production build reads public Supabase variables at build time. Ensure .env.local exists, or pass placeholder values on the command line as the CI workflow does. A successful build does not require live credentials.

Login redirect loop or "invalid redirect URL". Your Supabase auth configuration does not match the app URL. Under Authentication then URL Configuration, set the Site URL and add <your-url>/auth/callback to the redirect URLs for both http://localhost:3000 in development and your production domain.

Signup is rejected. The app restricts signups to a configured email domain. Check NEXT_AUTH_DOMAIN in your environment and make sure the address you are testing ends with that domain.

Cron endpoints return 401. The scheduler is not sending the bearer token, or it does not match. Confirm CRON_SECRET is set in the deployment environment and that your scheduler sends Authorization: Bearer <CRON_SECRET>.

Emails are not delivered. The RESEND_FROM_EMAIL must use a domain verified in Resend. Unverified domains are silently dropped. Verify the domain in the Resend dashboard and check the API key is valid.

The AI assistant does not respond. the assistant is optional and disabled without GROQ_API_KEY. Add a valid key, or comma-separate several keys for load balancing. The provider is OpenAI-compatible and can be swapped in app/api/chat/route.ts.

RLS errors or empty data for a valid user. A user is missing a role or department, so Row Level Security filters everything out. Check the user's row in the Supabase table editor and confirm the role and department are set.

SSO login does nothing or the password form keeps showing. The email domain has no active row in sso_connections. Add one under Admin, Single Sign-On, set it active, and confirm the provider app or SAML connection is configured in the Supabase dashboard under Authentication, Providers or Authentication, SSO.

Leave accruals grant nothing. Only balances with an accrual_rate greater than zero accrue, and a balance only grants once per elapsed calendar month. If the migration has not been applied the preview reports a pending migration; run migration 025 first.

GDPR export returns 403. Exporting another member is restricted to administrators. A non-admin can only export their own data, which is the default when no userId is supplied.

Read next

Project links

Clone this wiki locally