Entity-Relationship Diagrams
The database schema, drawn from the Flyway migration SQL in
backend/src/main/resources/db/migration/
(column and key names verified against the DDL — e.g. the employee PK really is
employee_number, and timesheet.employee_id references it). How the schema
is owned and changed is covered in
backend/entities-and-migrations.md; what
the payroll tables mean is in business-rules.md.
91 tables total. This page diagrams the payroll core, the statutory rate tables, and the recruitment chain; the remaining domains are summarized in §4.
1. Payroll core
Everything the payroll engine reads or writes, plus the RBAC tables that gate
it. Singular table names are as-is from the DDL (users is plural only to
dodge the SQL reserved word). payroll_settings is a single-row policy
table (see business-rules.md) with no FKs, shown here
because every generation run reads it.
Reading notes, straight from the DDL:
employee.employee_numberis the PK (V2); every HR/payroll table'semployee_id(oremployee_numberinbonus/employee_deductions) references it. The four government ID columns are eachUNIQUE— which is why the service layer pre-checks them to return a friendly 409 (backend/README.md §2).- RBAC (
V1):users.role_idis the primary role,user_roleadds extra roles, androle.parent_role_idforms a hierarchy whose permissions are unioned at login.users.failed_login_attempts/locked_untilwere added byV75alongsiderefresh_tokens. payslipis a snapshot, not a join: it denormalizes employee name, position, department, government IDs and every computed amount at generation time, so historical payslips survive later employee edits.payslip_deduction_items.employee_deduction_idis nullable (ON DELETE SET NULL) for the same reason — thelabel/amountsnapshot outlives the assignment (V70).- Not drawn but in the same domain:
payroll_transactions,payroll_changesandpayslip_history(append-only audit tables hanging offpayroll/payslip,V4),ph_holidays,work_suspensions,de_minimis_benefit_types,company_profile,government_filing, and theuser_logaudit trail written byAuditInterceptor.
2. Statutory rate tables — the effective-date cohort pattern
Four standalone reference tables carry the government schedules. None has an
FK; the payroll engine joins them by salary range and date: a lookup takes
the bracket whose [salary_bracket_from, salary_bracket_to] contains the
monthly salary, from the cohort with the greatest effective_date not after
the payroll period end (findBracketAsOf, with an earliest-cohort fallback
for historical periods). Rates are never UPDATEd in place — a new statutory
year is a new set of rows with a new effective_date.
sss_contribution_ratesgot its official 2026 shape (per-bracket MSC and Regular/EC/MPF share breakdown, 61 brackets) inV45.- In
philhealth_/pagibig_contribution_rates, a share value < 10 is a percentage of salary, otherwise a fixed peso amount — that's how the fixed floor/ceiling brackets and the percentage middle band coexist in one table (V12, interpreted inPayrollServiceImpl.calculatePhilhealthDeduction). withholding_tax_bracketsadditionally cohorts bypay_period(DAILY/WEEKLY/SEMI_MONTHLY/MONTHLY/ANNUAL) sinceV63; tax =base_tax + rate_on_excess% × (income − taxable_income_from).
The actual 2026 numbers live in business-rules.md §5–6.
3. Recruitment chain
From V14
(plus the 1:1 resume table from
V77).
FKs to department/position/employee exist on most of these
(requester, interviewer, referrer, offer creator) but are omitted here for
readability.
The functional flow (requisition → opening → applicant → offer → hire) is documented in modules/recruitment.md.
4. The other domains (counts only)
Not diagrammed — their structure is conventional and the DDL is short:
- CRM — 9 tables (
V15,V89):crm_organization,crm_contact,crm_lead,crm_deal,crm_activity,crm_note, plus the settings layer —crm_picklist_item(pipeline stages / lead sources / activity types),crm_settings(singleton), andcrm_settings_changes(field-level audit). - Inventory / warehouse — 19 tables (V17–V34): items, categories, suppliers, warehouses + location hierarchy, per-warehouse and per-store stock, stock transactions, purchase receiving, serial/lot tracking, transfers, pricing + price history, orders and costing.
- Customer portal — 4 tables
(
V36,V37):portal_user(its own auth table — portal accounts are not ERPusers),customer_order,customer_order_item,shipment_tracking. - Billing — 2 tables (
V52): single-rowbilling_subscription+billing_webhook_eventledger (backend/billing.md). - Communications — 7 tables (
V7):conversation,messages,message_attachments,message_folders,message_status,notifications,announcement.
The complete per-domain table inventory is in backend/entities-and-migrations.md §3.
5. Notable indexes
Spot-checked CREATE INDEX statements worth knowing about:
- Grid filter indexes
(
V41): the AG Grid server-side datasources filter/sort on non-FK columns, so the unbounded-growth tables got targeted indexes —timesheet(status),timesheet(work_date),payroll_transactions(transaction_date)/(transaction_type),user_log(log_date_time), andemployee(last_name/first_name/status/date_hired). - Chat cursor pagination
(
V25):messages(conversation_id, message_id DESC)— matches the "newest page first, walk backwards by id" query exactly. - Rate cohort lookups: every statutory table indexes
effective_date;withholding_tax_bracketsadds(pay_period, effective_date)(V63). - Uniqueness as business rules:
ph_holidays (holiday_date, holiday_name)unique (V56) makes holiday seeding idempotent;billing_subscription (provider, provider_subscription_id)unique (V52) makes webhook application idempotent; the four government-ID uniques onemployeeback the 409 pre-checks. - Auth:
refresh_tokens(user_id)andrefresh_tokens(family_id)(V75) — family revocation on token-reuse detection is one indexed scan. - Per-user features:
saved_view(user_id, entity_key)(V82);employee_deductions(employee_number, active)(V69) for the "applicable deductions" lookup during payslip generation.