Security & privacy

What protects the data, and what does not

An HR database holds NRIC numbers, salaries, medical certificates and home addresses. Below is what the application does about that, followed by the limits — because a security page that only lists strengths is marketing, not documentation.

Authentication

Password storage
bcrypt, cost factor 12. Plaintext is never stored or logged.
Password policy
Minimum 8 characters with upper case, lower case, a digit and a symbol.
Brute force
Failed attempts are tracked per account in the database and throttled.
Two-factor authentication
TOTP with single-use backup codes, enrolled by each employee from their profile. An administrator can require it company-wide, after which anyone who has not enrolled is sent to set it up before they can use the portal. The secret is encrypted at rest with the same key as identifiers.
Sessions
Signed JWTs with an 8-hour expiry, re-validated against a per-employee session version.
Revocation
Role change, termination or password reset bumps the session version and invalidates live sessions immediately.
Password reset
Single-use tokens with a one-hour expiry, delivered by email. New joiners activate their own account; nobody is emailed a password.

Data protection

NRIC
AES-256-GCM authenticated encryption at rest, using a key you generate and hold. Every decryption writes an audit entry.
Sensitive display
NRIC and salary are masked in the interface until an admin explicitly reveals them.
Uploaded files
Receipts, medical certificates, policy PDFs and photos live in private object storage and are served only through an authenticated route that checks session and tenant first.
In transit
TLS enforced, with HSTS set to a two-year max-age.
Data minimisation
Bank details and statutory payroll classification were removed from the schema entirely when payroll was withdrawn.

Access control

Roles
Employee, approver, admin, super admin and managing director, checked server-side on every mutation.
Tenant isolation
Every query is scoped by company. A record from another tenant reads as not-found, not forbidden.
Elevated data
Management notes are restricted to super admins and the managing director; a plain admin cannot read them.
Destructive actions
Employee deletion is a two-step request-and-approve flow, not a button.
Audit log
Append-only. Identifier decryption, entitlement overrides, role changes, deletion approvals, exports and approval overrides are all recorded.

Application hardening

Input validation
Zod schemas on every server action and API route.
SQL injection
Parameterised queries throughout via Drizzle ORM; no raw SQL built from user input.
SSRF
Server-side fetches validate the target against an allowlist before making a request.
Content-Security-Policy
Scoped script, style, image and connection sources, with frame-ancestors, base-uri and form-action locked down.
Uploads
MIME-type allowlist and size limits enforced server-side on every endpoint.
Rate limiting
Sliding-window limits on signup, password reset and other unauthenticated write paths.
Secrets
Environment variables only, validated at startup. None in source.

Limits, stated plainly

Security depends on your deployment

The application controls listed above hold regardless of host. Database encryption at rest, network isolation, patching and backup security are properties of the infrastructure you chose, and they are yours.

No independent audit

This codebase has not been penetration-tested or certified by a third party. The source is open, which means you can review it or commission a review — not that anyone else already has.

Third parties see some data

Your database host sees everything. Your object store sees uploaded files. If enabled, your email provider sees notification content and your OCR provider sees receipt images. Each needs a processing agreement.

Open signup is on by default

Anyone reaching /signup can create a company on your instance. For a single-company deployment, block that route at your proxy once you are set up.

Reporting a vulnerability

If you find a security issue, please report it privately rather than opening a public issue. The disclosure process and contact details are in SECURITY.md in the repository.

Verify it yourself

Everything on this page is claims about code you can read. The access-model guide names the specific files to check.