01 / WORK / CASE STUDY
Multi-tenant SaaS HRIS
A multi-tenant HR platform — attendance, scheduling, payroll, approvals, recruiting — across 36 modules; currently in acceptance and remediation.
- TYPE
- FULL STACK
- SCOPE
- Internal · acceptance stage
- STACK
- DJANGO · DRF · MYSQL · REDIS · CELERY · REACT
- LINKS
- Internal project (private)
01 / PROBLEM
HR in small and mid-sized companies is scattered across spreadsheets, paper approvals and disconnected tools — hard to audit, easy to get wrong, and re-implemented at every company. This system is a multi-tenant SaaS: one deployment serves multiple organizations with strict data isolation, subscription-gated modules, and built-in Taiwan labor-law compliance checks.
02 / CONSTRAINTS
- Company-internal; the codebase is private. Currently in acceptance with synthetic seed data — not yet in production.
- Tenant isolation is non-negotiable — a single query missing its tenant filter is a data breach.
- Must implement Taiwan labor law (working-hour caps, overtime multipliers, withholding) and support three languages.
03 / ARCHITECTURE
A modular monolith: Django 4.2 + DRF split into 36 domain apps (attendance, scheduling, payroll, approvals, recruiting, assets, labor insurance…), with ~215 models and 188 registered resource routes — 728 concrete endpoints exercised during acceptance. Deliberately not microservices: payroll and approval flows are far simpler inside one transaction boundary.
Multi-tenancy is shared-schema with row-level isolation: middleware resolves the tenant and 304 classes inherit the tenant-aware base. Async work runs on Celery + Beat — shift generation, attendance archiving, holiday/regulation sync; Redis backs cache and channels; the database switches between PostgreSQL and MySQL per environment. CI runs on GitHub Actions with pre-commit, plus a Prometheus / Grafana / Loki observability stack.
04 / RESPONSIBILITIES
Independent full-stack development (all 434 backend and 123 frontend commits are mine, across two git identities): data models and multi-tenancy, the permission system, domain modules, Celery jobs, the React frontend and CI.
05 / CHALLENGES → SOLUTIONS
CHALLENGE
A security audit found custom API actions bypassing tenant filtering — accounts could be manipulated across tenants.
SOLUTION
Isolation was pushed into the base layer (TenantAwareModel plus middleware-resolved context) and every queryset-bypassing action patched. The audit confirmed 35 findings (2 critical); 30 are fixed and 5 partially — pending staging re-verification, recorded as-is.
CHALLENGE
The payroll API's cache key ignored user identity — within one TTL window, an employee could hit an admin's cached response and see company-wide salaries.
SOLUTION
Added vary_by_user to the cache decorator, folding the user into the key — then wrote a regression test with real HTTP calls proving two users can never see each other’s cache.
CHALLENGE
Taiwan's public holidays and regulatory parameters change yearly — manual syncing doesn't scale.
SOLUTION
Scheduled it with Celery Beat: yearly next-year holiday sync, daily regulation-expiry checks, weekly upcoming-holiday notices — tasks retry automatically with exponential backoff.
06 / SYSTEM FACTS
36
domain modules
215
data models
728
endpoints tested
87.5%
acceptance pass rate
07 / LESSONS
Cache design is authorization design: the payroll leak came from a cache key missing the identity dimension. In a multi-tenant system that class of bug is not a performance issue — it is a breach.
Systemic vulnerabilities trace back to model design: dual global-vs-tenant role tracks caused the privilege and cross-tenant issues. Doing it again, I would design permissions on the tenant dimension from day one.
NEXT
AI Nail Platform