An enterprise HR platform for a Japanese client — leave, attendance, expenses, work logs and compliance — first built on GeneXus + WorkWithPlus, then re-platformed to React, FastAPI and PostgreSQL by working embedded with the client, without losing a single record.
→ 92,579 records migrated from the legacy system, with financial totals reconciled exactly
ELM is the HR and operations platform a Japanese enterprise client runs on every day: leave, attendance, expenses, work logs, the organisation directory, announcements and internal administration, in English and Japanese.
I worked on it twice. First, we built it on GeneXus + WorkWithPlus, the low-code stack the client's ecosystem already ran on. Later, when the system had outgrown that platform, I led its re-platforming to a modern stack — React and TypeScript on the front, FastAPI and PostgreSQL behind — while the client kept using it.
The second phase is the part I'm proudest of, because it was less about code and more about how the work was done: close to the client, feature by feature, with nothing lost on the way.
Phase 1: Built on GeneXus + WorkWithPlus
GeneXus let a small team ship a large surface quickly: leave policies and approvals, the monthly attendance plan, expense applications, work registration, reports and email templates. Where the stock components fell short, I built custom GeneXus user controls wrapping JavaScript libraries — a Gantt chart, a Kanban board and a rich-text editor — several of which are now open source.
It worked, and people relied on it. But over time the limits showed:
Access control was a single "user type" number. Every new permission rule meant another special case.
No change history. Nobody could answer "who changed this, and what was it before?"
Notifications were split across separate mail and push tables.
Languages were hard-coded, not chosen per user.
Sessions depended on the platform's built-in access manager.
None of these were bugs. They were the ceiling of the platform.
Phase 2: Re-platforming, the forward-deployed way
The goal was not "rewrite it in React". It was: give the same people a better system, without changing how they work on day one, and without losing anything.
Parity first, then improve
Users had years of habits built on the old screens. So the first rule was parity: the working-time calculation, the notification type numbers, the leave rules, even the names of downloaded receipts were kept exactly as legacy did, and each decision was written down. Improvements came after parity, one at a time, each tracked as its own task — around 60 of them in the user guide alone.
Re-scope when the facts change
Early designs treated ELM as a multi-company SaaS product, with tenants, subscriptions and billing. Working with the client made it clear that wasn't the need: one company would run it. So we removed that whole layer in September 2026. Less code, fewer screens, nothing to maintain that nobody would use.
Migrate, then prove it
Moving the data was the riskiest part. Users, leave allocations and requests, attendance history, expenses, work logs, announcements, release notes and the notification archive — 92,579 records — were migrated, and financial totals were reconciled exactly against the old system.
Two things were left behind on purpose: old login-session history, and raw punch records the old system had itself already superseded. Saying clearly what you didn't migrate matters as much as what you did.
What changed for the client
Area
GeneXus version
Modern ELM
Access control
One user-type number
8 named roles built from fine-grained permissions, plus custom roles
Change history
None
Every create, update and delete recorded in an immutable audit log
Read history
None
A separate log of who viewed sensitive records (GDPR / PDPA style)
Sign-in
Username and password
Email and password, or Google sign-in for company accounts; short-lived rotating tokens
Notifications
Separate mail and push tables
One inbox and outbox, per-user language, Slack integration
Languages
Hard-coded EN / JA
EN / JA chosen per user, including how names are displayed
Compliance
Manual
Japan's Article 36 overtime limits tracked and flagged in reports
Expenses
Manual export
Generated PDF summaries and bulk ZIP download of receipts
Technical Stack
Frontend: React, TypeScript, role-aware routing and menus driven by the server
Backend: Python, FastAPI, background workers for email, Slack and scheduled jobs
Data: PostgreSQL with separate schemas for identity, HR data and audit; database triggers write the audit log
Caching: Redis cache-aside for high-read paths, falling back to PostgreSQL when Redis is down
Infrastructure: Docker for local development and services
Legacy: GeneXus + WorkWithPlus, with custom user controls in JavaScript
Technical Decisions
Permissions as data, not code
The old "user type" number became named roles built from permission keys like leave:approve or expense:process. The server decides what each role sees, down to which blocks appear on the home page, so the frontend never guesses.
An audit log nobody can edit
Audit records are append-only, written by database triggers rather than application code, so even a bug in the API can't skip or alter them. Reads of sensitive records go to a second log. Together they answer the questions the old system never could.
Documentation verified against the code
The user guide is checked against specific commits, and anything not re-verified is marked as such. For a client who has to trust a new system with their HR data, knowing exactly what is true today is part of the product.
Outcome
The client moved off the legacy platform in 2026 with its full history intact: 92,579 records, financial totals reconciled exactly.
Every screen in the release works; there are no placeholder pages.
The system now has the audit trail, access control and per-user language support the old platform couldn't provide.
The client and some details are kept private. Happy to walk through the architecture and migration approach in more depth in conversation.