Skip to content
MUNEEB SHAFIQ
PRODUCTION FULL-STACKF04

Symufolk HRIS

Production HRIS for Pakistani SMEs: encrypted personal data, per-client deployment, 123 endpoints

  • FastAPI
  • SQLAlchemy 2.0
  • Alembic
  • PostgreSQL
Status
Active, 47 commits between 3 Aug 2026 and 25 Aug 2026, no commits since. Deployed to Render, Vercel, Supabase and Cloudflare R2. The CORE-2 application-security wave is complete; CORE-8 production hardening is partial, with the plan naming HTTPS, a rehearsed backup restore, an emptied DEMO_TODAY and one reconciled parallel month as what remains before real employee data.
Live demoPrivate repository

By the numbers · 10

  • 123

    REST endpoints across 12 domain packages

  • 215

    backend test functions across 21 pytest modules

  • 83 files, -5,369 lines

    payroll and compensation removal (PHASE R)

  • 32 of 36

    requirement modules still in scope after the confidentiality decision

  • 10

    Alembic migrations owning the production schema

  • 58

    employee records in the demo tenant

  • 286

    source files outside the vendored Node toolchain

  • ~307 KB

    architecture, security and task documentation

  • 17

    design-system primitives in the frontend

  • 3

    CI jobs, two of which are grep-based convention gates

Summary

A people-first HR information system for Pakistani and South Asian SMEs: a React SPA over a FastAPI modular monolith where twelve domain packages each own their routers, services and models behind one deployable. The architecture was tested by a live scope reversal, when the client classified salary and job-grading data as confidential mid-build, the payroll domain was deleted rather than feature-flagged, removing 5,369 lines across 83 files with the rest of the system still green. Security is shipped rather than backlogged: national identity numbers are Fernet-encrypted at rest, every mutation writes an audit row inside the same transaction as the change, and the app refuses to boot outside development if a production secret still holds its development default.

The problem

Small and mid-sized employers in Pakistan run people operations out of spreadsheets and messaging apps: attendance exported from a biometric device, leave balances in a workbook nobody wants to touch, offer letters retyped each time, and copies of national identity documents scattered across inboxes. Off-the-shelf HR products are priced and shaped for larger Western companies and ignore local realities such as provincial holidays and per-company installation. The build was commissioned against a 36-module requirements document from a People and Culture function, then narrowed to 32 modules when that function withdrew all salary and grading data as confidential.

Approach

  1. Structured the backend as a modular monolith: one deployable, twelve domain packages under app/domains (auth, org, employees, attendance, leave, recruitment, onboarding, docs_policies, comms, compliance, expenses, dashboard), each exposing a router that fails the boot loudly if it cannot import.

  2. Kept routers thin and pushed rules into services, leave working-day maths excludes weekends and non-optional holidays in one place; the expense claim state machine declares every legal transition and its authorising role in a single TRANSITIONS table so a new status cannot silently become reachable.

  3. Wrote a five-line audit helper that domains call inside the same transaction as the change, so the trail can never drift from the data; the compliance domain then queries that log, and even a dry-run retention preview is audited.

  4. Encrypted employees.cnic with a Fernet TypeDecorator keyed from an environment variable, with a version-byte check that makes the data migration safe to re-run and an explicit note that any future CNIC search needs an HMAC blind index rather than a LIKE.

  5. Executed the confidentiality decision as a deletion, not a flag: PHASE R removed the payroll domain, salary columns, the L1–L13 grade structure and three statutory registers, then added a CI job that greps the tree and fails the build if payroll vocabulary reappears.

  6. Layered account security in complementary pairs, per-IP fixed-window rate limiting on the two unauthenticated write paths, per-account lockout, password reset, and TOTP with hashed recovery codes whose enforced roles are configuration rather than code.

  7. Put file uploads behind a three-method storage interface with a local-disk default and a Cloudflare R2 backend, so receipts and documents were built and tested before any bucket existed and the move to R2 was four environment variables.

  8. Handed the schema to Alembic outside dev, kept create_all for development only, and added tests/test_migrations.py so a model change without a migration fails CI.

Architecture

symufolk-hris · flow
React SPA (Vite)Vite proxy or Vercel rewriteFastAPI /api/v1JWT decode + role guarddomain routerdomain serviceSQLAlchemy 2.0PostgreSQL (Supabase) | append-only audit log written in the same transaction; file uploadsstorage interfaceCloudflare R2 or local disk
ComponentRole
web/src/modulesFifteen feature module folders (people, attendance, leave, recruitment, onboarding, expenses, compliance, helpdesk, analytics, performance, learning, engagement, screening, dashboard, settings), lazy-loaded per route
web/src/uiSeventeen Symufolk Design System primitives (Button, Table, Dialog, Tabs, Toast, Tooltip and others) over a CSS token file
web/src/apiFetch client and hooks that attach the JWT to every request and log out on a 401
app/coreCross-cutting layer: settings with production secret guards, SQLAlchemy engine, JWT and bcrypt security, the audit helper, a single configurable clock, rate limiting, the Fernet EncryptedString type, and the storage abstraction
app/domainsTwelve packages of router + service + models + seed, totalling 123 HTTP endpoints; recruitment additionally exposes the only unauthenticated routes in the system, a public careers list and apply endpoint
Audit logAppend-only table written by domains inside the change transaction; queried by the compliance domain and by retention runs, including dry runs
Alembic migrationsTen revisions owning the schema outside development, including a re-runnable CNIC encryption migration; test_migrations.py fails CI on a model change without one
CI (GitHub Actions)Three jobs, backend pytest, frontend build and test, and a conventions job whose two greps fail the build if payroll vocabulary returns or a domain reads the wall clock instead of core.clock
Excel on-rampNode importer that validates a filled HR workbook for duplicates, dates, unknown managers and circular reporting chains, and refuses to write anything while errors remain

Trade-offs

  • Chose

    One deployment and one database per client

    Over

    Shared-schema multi-tenancy with tenant_id columns and PostgreSQL row-level security

    ADR 001 records that the product is installed company by company rather than self-serve, so a shared schema would add a column to roughly 40 tables, turn about a dozen unique keys into composites, and require a per-endpoint leak test, while a single forgotten WHERE clause would leak one company's staff records to another. The accepted cost is per-install upgrades and no cross-company reporting.

  • Chose

    Deleting the payroll domain outright

    Over

    Hiding it behind a feature flag or leaving it dormant

    The client classified the data as confidential, so the tables, engine, tests and screens all had to go; git history preserves it if the decision reverses. The measured cost was 83 files and 5,369 lines, and backend tests dropping from 109 to 73, payroll had been the most tested part of the system.

  • Chose

    Anonymising an ex-employee in place on erasure

    Over

    Deleting the employee row and its history

    retention.py notes that a hard delete would take the company's statutory record with it and leave holes in every historical headcount; identity fields are cleared, documents and receipt images are dropped from object storage, logins are deactivated, and the audit row names the fields cleared but never their values.

  • Chose

    Supabase Postgres plus a nightly pg_dump from GitHub Actions

    Over

    Render's managed database and paid cron jobs

    render.yaml records that Render's free Postgres is deleted after 30 days while Supabase's is only paused, and that the system measures in tens of megabytes a year because no file enters the database. The free tier takes no backups, so backup.yml dumps to the R2 bucket that already exists, at the cost of database credentials living in GitHub secrets as well as Render's.

  • Chose

    A shallow /health liveness probe for the platform and a database-touching /health/ready for an external monitor

    Over

    One health endpoint used by both

    main.py argues that pointing Render at the readiness check would let a ten-second database blip restart a service that was answering fine, turning a hiccup into a cold start.

  • Chose

    In-process fixed-window rate limiting

    Over

    Redis-backed counters

    The code marks the limits as per-worker and flags the move to Redis for when the deployment runs more than one uvicorn worker; per-account lockout is the deliberate complement, since it does not depend on the client address, which is spoofable until a trusted proxy overwrites X-Forwarded-For.

At scale

  • Twelve backend domain packages containing 54 Python files under backend/app/domains.

  • Fifteen frontend feature module folders and 147 files under web/src.

  • Ten Alembic revisions, including a re-runnable migration that encrypts employees.cnic and verifies a decrypt round-trip on every row before committing.

  • Twenty-one backend test modules covering auth, MFA, password reset, rate limiting, crypto, storage, retention, migrations and eight business domains.

  • Nine architecture and process documents totalling roughly 307 KB, including one accepted ADR and a 652-line implementation plan with per-screen status and numbered tasks.

  • 47 commits between 3 and 25 August 2026, across six merged pull requests.

  • Deployed across four managed services: Render (API), Vercel (SPA), Supabase (PostgreSQL) and Cloudflare R2 (documents and receipts), with a nightly pg_dump from GitHub Actions.

My role

Repository owner and reviewer. Of the 47 commits on main, 13 are authored by munib123 (Muneeb Shafiq), the initial codebase push on 3 Aug 2026, the backend wiring and architecture diagrams on 12 Aug, Vercel asset caching and security headers, the CORE-10 Microsoft Entra SSO design document, the receipt progress-bar work, and all six pull-request merges. The remaining 34 commits are authored by a collaborator and cover PHASE R, the CORE-2 security wave, expense claims, Alembic, CI and deployment. The contribution split must be confirmed with Muneeb before this is described as solo work anywhere on the portfolio.