Skip to content
← All articles

Oct 13, 2026
4 min read

  • Open Source
  • NestJS
  • TypeORM
  • MikroORM
  • Angular
  • Electron

Ever Gauzy is free software that runs a whole company — HR, clients, projects, hours and invoices — in one place. What makes it different, the 53 improvements I contributed, and what I learned.

The project

In one sentence: Ever Gauzy is free software that runs a company — employees, clients, projects, working hours, invoices and expenses — in one place.

Picture a 30-person agency. Today it probably pays for one tool to track hours, another for invoices, another for holidays and HR, and a spreadsheet for sales. None of them talk to each other, so someone copies data between them every week. Gauzy replaces that pile with one application: the same employee who logs time on a project is the one who appears on the client's invoice and in the holiday calendar.

It's open source, so a company can use it for free and host it on its own servers, keeping its data at home. There is also a web version, a demo, and desktop apps for Windows, macOS and Linux.

What makes it innovative

  • Everything in one place, and free. Suites that do everything usually lock companies in and charge per user. Gauzy offers the same range — HR, sales, projects, hours, invoices — as software a company can own.
  • No more copying data between tools. Because every part shares the same people, clients and projects, a question like "how much did this project cost us?" is answered in a click.
  • Made for remote teams. A desktop app records working time so managers and clients see real hours, not estimates.
  • Grows with the company. Several companies, offices, currencies and languages can live in one installation, and add-ons extend it — including ways for AI assistants to work with it.

In short: my contribution and what I learned

My part: I fixed 53 problems in Gauzy, all accepted by the maintainers. Most were cases where the software gave different or wrong answers depending on how it was installed — deleted invoices still counted in statistics, hours counted several times, appointments shown 12 hours late. One was a privacy issue: any employee could read every colleague's notifications. I also fixed screenshots on computers with several screens.

What I took away: when a product promises to work "everywhere", every variant must be tested — bugs hide in the version nobody uses day to day. And small, focused improvements get accepted far faster than big ones.


The rest of this article goes into the technical details, for developers.

Under the hood

A large Nx monorepo (8,000+ GitHub stars, AGPL-3.0) — NestJS API, Angular web app, Electron desktop apps, around 80 plugins, PostgreSQL / MySQL / SQLite.

In detail: what I brought

My contribution in numbers: 53 pull requests, all merged (+5.9k / −1.1k lines), almost all with Jest specs.

TypeORM / MikroORM parity

About twenty PRs come from one exercise: take a feature, read both branches, find where MikroORM says something different.

  • Unknown operators matched everything. LessThan, LessThanOrEqual, Like and ILike were translated to {}, which MikroORM reads as no condition. A "maximum amount" invoice filter returned every invoice. I added the missing translations.
  • ILIKE on MySQL and SQLite. MikroORM emits $ilike verbatim, which those databases reject. I added isMikroOrmPostgres() (the existing check always answered "postgres" under MikroORM) and two helpers, mikroOrmContains and mikroOrmILike, now used by task, tag, employee and candidate search.
  • Deleted data leaking through raw Knex. Raw Knex bypasses MikroORM's soft-delete filter: the public /stats/global counted deleted invoices, budget reports counted deleted time. Each read now has its whereNull('deletedAt').

Permissions in a multi-tenant app

The most important fix is #10495. The base service scopes queries by employeeId, but notifications use receiverEmployeeId — so any employee could read the whole tenant's notifications. I added a receiver scope to every read, including each branch of an OR condition. Nearby, mentions now reach the mentioned person, and removing a team member actually removes them.

Numbers that were wrong

  • A 1-hour log split into six time slots counted as 6 hours (the join multiplied it).
  • Tag usage was inflated by 25 LEFT JOINs; fixed with COUNT(DISTINCT …).
  • 'YYYY-MM-DD hh:mm:ss' without AM/PM shifted times by 12 hours in three places — afternoon appointments were drawn at 2 a.m.

Sorting, search and the desktop timer

An allowlisted parseSortOrder() so tables really sort, splitKeywords() so a trailing space no longer matches every row, and in the Electron timer, multi-monitor screenshots that picked the wrong screen (only the cursor's x was compared) or captured nothing at all on some Linux setups.

Technical lessons

  • An abstraction leaks exactly where nobody tests it. Supporting two ORMs is a promise that both answer the same question the same way. The bugs lived in the branch nobody ran locally. Since then, I test the branch, not just the feature.
  • Raw queries skip the safety nets. Global filters (soft delete, tenant scope) only protect code that goes through the ORM. Every knex(...) is a place where you must re-apply them by hand.
  • Permission filters depend on naming. An automatic filter keyed on employeeId silently ignores a table that calls the same thing receiverEmployeeId. Security that relies on conventions needs a test per exception.
  • One helper beats eight fixes. splitKeywords() fixed eight call sites at once and keeps the next one correct.
  • Small, scoped PRs get merged. 53 focused PRs moved faster than a few big ones would have.