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.