Skip to main content

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