An ERP for project operations at Peopleshift Group, covering budgets, cash advances, learning-partner bookings, receipts, invoices, and payments across every training engagement that Squadgames, ShiftAcademy, and Peopleshift run.
PeopleShift Group is an Indonesian corporate-training company offering leadership programs, team-building games, and academy cohorts, delivered under three sister brands: PeopleShift, Squadgames, and ShiftAcademy. Over twelve hundred sessions delivered, more than five hundred companies served, with clients including PLN, Pertamina, Toyota, Bukalapak, BRI, and Prudential.
Every engagement is a project, and a project is a small economy of its own. A budget is agreed with the client. Learning partners, external facilitators, are booked to run the session. The ops team on the ground buys materials, pays venues, and collects receipts. Finance, on the other side, issues the invoice and tracks whether the client has paid and whether the facilitators have been paid. Multiply that by hundreds of projects a year across three brands, and the flow is not something a tool designed for a single SaaS company handles well.
When I started, the finance and ops teams were running the whole operation on a mix of Google Sheets, Discord threads, PDF invoices, and Word documents. They had looked at the usual off-the-shelf ERPs. Nothing fit. The shape of a training-and-events business is specific enough that "configurable" software still forces you to bend your process to theirs. The ask was blunt: build us something that fits us, keep it cheap, and ship it this quarter.
Three weeks later, a usable prototype was live for the ops and finance teams, built end-to-end by a single developer working heavily with AI tooling. It has been in production and under continuous development ever since.
This wasn’t the kind of project where you write a big architecture document and then hire a team. It was the kind where you ship something in three weeks or the ops team quietly gives up and keeps emailing PDFs. One developer, part-time, alongside the rest of my work. The budget was small by design: the whole point of building custom was to beat the cost of an off-the-shelf tool that wouldn’t have fit anyway.
The architecture question I spent the most time on was not which framework, a Next.js monolith was obvious from the first week, but how to keep the surface area small enough for one developer to own end-to-end, for years, while the scope kept expanding.
The shape settled quickly. Next.js 16 on the App Router handles rendering, routing, and the API. Supabase manages Postgres and storage. NextAuth v5 handles authentication, deliberately decoupled from the database layer so that swapping hosting providers later would not also mean rewriting auth. Data on the client is TanStack Query and TanStack Table. The entire ERP is effectively a large set of filterable, editable tables, and TanStack is what makes that boring in a good way. The mobile experience is the same codebase served as a PWA, so ops can upload a receipt from the field the moment it is in their hand.
Underneath everything is a single spine: the Project. Budgets, cash advances, learning-partner bookings, receipts, invoices, and payments all belong to a project. There is no separate "CRM" or "finance" module in the code, there are just views over the same relational model from different angles. That shape was chosen early and has absorbed every new requirement since without a rewrite.
The two decisions I am most pleased with did not live in the framework layer. The first was the import system: the ERP's tables and column names deliberately mirror the Google Sheets the finance and ops teams had been using, so migration was a paste-and-check, not a rewrite. Nobody had to re-learn their data to move in.
The second is the error pipeline. With one developer in production, I cannot afford to find out about a broken invoice form on Monday morning from a Discord message that starts with "hey, quick one". Every unhandled error, client and server, flows through a classifier that tags it Critical, High, Medium, or Low. Critical errors fire a Discord webhook and open a Jira ticket automatically with the full context. Everything is logged to Postgres with dedup, so repeat errors count up instead of spamming. It is not glamorous, but it is the thing that lets me sleep.
A usable prototype went live in three weeks, with finance and ops piloting it while the spreadsheets remained on standby for a few weeks longer. Migration was quiet. Because the ERP's tables mirrored the existing sheets, the move was a paste-and-check: the finance lead imported her own budget tracker on the second day without me present.
Ops changed the fastest. Receipts used to arrive as PDFs built in Word, emailed in weekly batches when they arrived at all. With the PWA, someone on the ops team can pay a venue, photograph the receipt, and have it attached to the correct project before walking back to the car. The weekly ritual of chasing missing paperwork largely disappeared, and ops estimate their administrative load dropped by around 80%. HR and GA began using the system as well, though neither was in the original scope, which suggested the thing was well-shaped enough to absorb adjacent work.
Finance changed next. Weekly transfers, checking what ops had spent, matching invoices, approving payouts to learning partners, used to take hours and now take minutes. Monthly close went from a multi-day reconciliation across half a dozen sheets to a filterable view of the same relational model. Finance put their saving at around 60%.
The ERP now tracks hundreds of projects across the three brands, with several thousand transactions flowing through it every month. Leadership has a live project budget summary, green, yellow, red, filterable by brand, that did not exist before. The system has been in production and under continuous development since it shipped. The spreadsheets still exist; they are archives now.
The decision I got right was refusing to redesign the finance team's data model. The tables, the column names, the way a project's budget is broken down into line items, all of that was already working for them, just trapped in spreadsheets. I copied it verbatim into Postgres and wrote an importer that took their own sheets as input. Moving in was not a training exercise; it was a paste. Most of the adoption wins above came from that one decision.
What I would do differently is set up CI and Playwright from the start, rather than waiting until several months in. In that interval I shipped more than a few bugs into production that a five-minute end-to-end test would have caught: small regressions that cost finance and ops real time before I noticed. The error pipeline catches problems after they occur. Playwright would have stopped them from occurring. The two should have been paired from day one.