When we hit a bug in a library we use on a project, we have two options. We can patch it locally and forget about it, or report it and send a fix to the library's authors. The first is faster today; the second is cheaper a year from now. We choose the second, and this article explains why we contribute to open source: it's an economic decision, not a gesture of goodwill.
We also describe the rest of our work with open code: our own tools published on GitHub, the rules under which we don't publish client code, and what "full rights to the code" means in a project built on libraries under other people's licences. At the end there's a list of things worth checking on any vendor's GitHub before you sign a contract with them.
- A fix left in a local copy of a library has to be reapplied with every update. A fix accepted by the authors is maintained together with the library.
- Our code has gone into more than 50 external open source projects, including PHPMailer, pydantic, Polly, FluentValidation, Monolog, Serilog, NLog, Ebiten, goquery, Pagefind and CommonMark.
- Client code is not published. We open up our own tools, such as vulp.
- "Full rights to the code" don't cover third-party libraries, which stay under their own licences. That's why dependency licences are checked before a project is handed over.
Four ways we work with open code
For us, open source means four different kinds of work that feed into each other. The first is client work: websites, admin panels, online stores and apps built for clients. The client receives the code as their property with the full commit history, not a sealed package. Some of the patterns and tools that come out of those projects later make their way back into our open repositories, without the client's own code or data, of course.
The second is our own open source projects, run from start to finish, with a public repository, releases and issue handling. The flagship example is vulp, described below. Next to it are smaller repositories: emulators, libraries, bots. On our open source page, projects that aren't ready for use yet are marked "in progress" rather than pretending to be finished.
The third is contributions to other people's projects. When we find a bug or a missing feature in a library our work depends on, we file an issue, open a pull request and go through the authors' review. There are more than fifty such projects, and together they have more than 400,000 stars on GitHub. We should say straight away what that number means: those are the stars of projects we contributed code to, not the stars of our own repositories.
The fourth is SaaS platforms, our own products where someone can sign up and work. Their core often shares code with our open tools, so a fix made in one ends up in the other. It's also why we don't build everything from scratch on a new client project: we have foundations we wrote ourselves and understand down to the last line.
A local patch versus a fix in the upstream library
Take a concrete case. A data validation library in a client project mishandles one edge case: a date field in a particular format passes validation when it shouldn't. The fix is a few lines. It can be applied in three ways, and each has a different cost spread over time.
The first way is a workaround in the project code: an extra check before calling the library. It works immediately, but it leaves code in the project that exists only because the library has a bug. A year later nobody remembers why the check is there, and when the library fixes the bug itself, the workaround stays as dead code that nobody dares to remove.
The second way is your own copy of the library, a fork, or automatically applying a patch to the installed package. The fix sits where it belongs, but every time the library is updated the patch has to be reapplied and checked against the changed code. When the library releases a security fix, a project on a fork only gets it once someone moves the changes across by hand. In practice, forks age faster than anything else in a project.
The third way is reporting the bug and sending a pull request to the authors. It's the slowest at the start, because you have to write a test that reproduces the bug, match the project's code style and wait for a review, which can take days or weeks. Once accepted, though, the fix is maintained by the library's authors, runs through their tests with every new version and reaches every other user. The client project goes back to the official version with no local modifications.
These approaches aren't mutually exclusive. A client project shouldn't wait for a review in someone else's repository, so a sensible setup looks like this: until the fix is accepted, the project runs on a temporarily pinned version with the patch, and once the official version with the fix is released, it switches back and removes the patch. What matters is that the temporary solution has an expiry date and doesn't stay forever.
Libraries our code has gone into
The list on our open source page isn't random. These are libraries we use on projects, so we find bugs in them during normal work, not by hunting for chances to contribute. They span several ecosystems, because we write in several languages.
| Library | Ecosystem | What it does |
|---|---|---|
| PHPMailer | PHP | Sending email from PHP applications, including over SMTP with authentication |
| Monolog | PHP | Logging in PHP applications, a standard in many frameworks |
| pydantic | Python | Type-based data validation and parsing |
| Polly | .NET | Resilience: request retries, circuit breaker, timeouts |
| FluentValidation | .NET | Object validation rules written in code |
| Serilog | .NET | Structured logging |
| NLog | .NET | Logging with configurable targets |
| Ebiten | Go | 2D game engine |
| goquery | Go | Searching and processing HTML documents |
| Pagefind | Rust / JavaScript | Serverless search for static websites |
| CommonMark | many languages | The Markdown format specification and its implementations |
There's a pattern here: many of these libraries handle validation, logging and resilience. These are layers that don't show up in a client project's interface, but they decide whether an error gets noticed at all and whether the application survives a brief outage of a third-party service. Those are exactly the places where edge cases only come out in production, with real data.
Knowing these libraries from the inside translates directly into client work. When logs stop being written or validation lets bad data through, the search for the cause starts in the library code, not on a Q&A forum. Someone who has been through review in a given project knows where its tests are, how it's built and which behaviours are intentional and which are bugs.
The numbers and the list change as the work goes on, so the current state is always on the page, not in this post. The catalogue of our own projects grows with what we actually release, not according to a plan of how many repositories it would look good to show.
Review by outside authors teaches more than review within your own team
In our own team, we set the coding rules ourselves. In someone else's project, the authors' rules apply, and you have to adapt to them before anyone reads the first line of your fix. Large libraries have a contributor guide, requirements for commit message format, and sometimes an obligation to sign a contributor licence agreement or a developer certificate of origin.
Automated tests in mature projects check a fix on many language versions and operating systems at once. A change that works for us on one version of PHP or .NET may fail on an older one the library still supports. That teaches you to think about backward compatibility, which then pays off on every client project, where updating one service must not break the integration with another.
Authors of popular libraries reject changes that are too large, bundle several fixes together or change the public API without justification. Being asked to split a pull request into three smaller ones is standard. After a few such conversations, smaller, single-topic changes become a habit in your own projects too, and that directly makes it easier for the client to review the history of the repository they have access to.
There's also the simplest benefit: code read by someone with no reason to be polite quickly shows its weak spots. The maintainer of a library with thousands of users won't accept a fix without a test just because its author is nice.
vulp: a tool we first built for ourselves
Our largest open project is vulp, a manager for working across many repositories at once, written in Rust, with a terminal text interface and a plain CLI. It solves a mundane problem: with several dozen repositories, manually checking which ones have uncommitted changes, which are behind the main branch and which have a pull request waiting means going into each directory one by one.
vulp shows the state of all repositories on one screen and lets you run operations in batches: sync, commit and push across many repositories at once. It handles pull requests, issues, task lists and statistics without leaving the terminal. It also enforces rules so that code in a bad state doesn't make it into a repository.
Rust is a good fit for a tool like this. A program that runs many times a day and queries many repositories in parallel should start instantly and shouldn't need a runtime installed on every machine. Rust compiles to a single executable, and its memory ownership model checks the correctness of concurrent code at compile time.
Opening up a tool takes work that an internal tool doesn't need: documentation for people outside the team, removing assumptions that only fit one setup, and error messages that make sense to someone who isn't the author. Its creators benefit from that work too, because the tool stops depending on one person's memory. The code on VulCodeCom's GitHub is there for anyone to clone, read and build their own solution on.
Public code as evidence: what you can check and what you can't
Many software house websites tell you they write clean, tested code. There's no way to verify that without access to the code. Public repositories are one of the few places where a claim about the quality of the work can be checked yourself, without asking the vendor's permission.
In a public repository you can see the commit history: whether changes are small and described, or arrive as one giant "initial" commit. You can see whether there are tests and whether the automated checks pass. You can see how the author responds to issues. Our open source page also has an activity chart for the last 90 days, so it's easy to check whether work on the code is regular or happened once, just before the page went live.
There are also things a public profile won't show. Most of the code written by a company that builds to order belongs to its clients and lives in their private repositories. Our accounts have more than two hundred repositories across more than a hundred projects, and a public profile won't show the private ones. The number of lines of code, more than five million in our case, says something about the scale of the work, not its quality: five million lines of bad code is still bad code.
That's why we treat public code as a sample to look at, not a certificate. It shows how we write when nobody is imposing a deadline, and how we behave in someone else's project, where we have no authority at all. On a specific project, what matters more is that the client has access to their repository and test environment throughout the project, not only at acceptance.
Licences: what you can put in a project the client gets full rights to
The client receives full rights to the code from us, plus documentation that lets them take the project over. Those rights cover the code we wrote. The open source libraries used in the project stay under their own licences, and those licences decide what the client can do with them. A store or admin panel built on the npm or Composer ecosystem easily reaches hundreds of transitive dependencies, so checking licences should be a separate step before the project is handed over, not a formality.
| Licence | Can it be used in a closed project | What you have to comply with |
|---|---|---|
| MIT, BSD | Yes | Keep the copyright notice and the licence text |
| Apache 2.0 | Yes | As above, plus the NOTICE file if the library has one, and marking your changes; the licence includes a patent grant |
| MPL 2.0 | Yes | Modified files of the library itself must be made available under the MPL; your own code next to them can stay closed |
| LGPL | Yes, with appropriate linking | Changes to the library itself must be made available under the LGPL; when distributing the application, allow the library to be replaced |
| GPL | Only without distribution | If a program that links to GPL code is passed on to others, the whole must be made available under the GPL with source code |
| AGPL | In practice, not in a closed service | Like the GPL, but the obligation to share the code also covers users who use the program over a network |
The difference between the GPL and the AGPL matters for web applications. The plain GPL is tied to distribution, meaning handing the program to someone else. An application running on your own server and accessed through a browser isn't distributed, so the GPL doesn't force you to publish the code here. The AGPL closes that gap: if users interact with a modified program over a network, they're entitled to its code. An AGPL library in a client panel can therefore mean an obligation to open up the panel.
In mobile and desktop applications it's the other way round, because distribution always happens there: the app ends up on the user's phone or computer. A GPL library in a mobile app means the whole app has to be made available under the GPL. That's why, on projects like these, licences are checked when choosing a library, not after half the app has been written.
The table describes general principles, not legal advice for a specific contract. With unusual licences, dual licensing or libraries with additional commercial terms, it's worth running the decision past a lawyer on the client's side.
A list of the licences of all dependencies, transitive ones included, can be generated with a single command in every popular ecosystem. It's worth adding the output to the project documentation, because when another team takes over the code, or during an investor's audit, it's the first question that comes up.
Generating the list is half the job. The other half is reviewing the entries marked unknown or custom, because the tools recognise a licence from a field in the package file, and authors sometimes put something there that doesn't match the LICENSE file in the repository. A package with no licence at all isn't "free to use": by default, all rights stay with the author.
What we don't publish
Code written for a client belongs to the client and doesn't go into a public repository, even if we think it would be useful to others. Nor do we publish fragments of it reworked so that nobody can tell where they came from. If the client decides to open up their project, that's their decision, and full rights to the code make it possible.
Opening up a client project takes more than switching the repository to public. You have to choose a licence, remove from the history everything that shouldn't be visible, and decide who will respond to issues and accept changes from strangers. A repository published without that decision quickly fills up with questions nobody answers, and that looks worse than having no public code at all.
What comes back to open source from client work is patterns and general tools, not specific code. For example, if we wrote a similar data export mechanism for three admin panels, a general library for that kind of export may appear in an open repository, written from scratch, without the business logic of any client. Fixes to external libraries don't contain anything from the project where we found the bug either, because the test that reproduces the bug is written on a minimal, artificial example.
We never publish production infrastructure configuration, keys, internal service addresses or test data taken from real systems. A repository that is going to become public has to be reviewed across its entire commit history before it's opened, not just in the current state of its files. A key removed in the latest commit is still visible in the history, so if it was ever there, it has to be revoked, not just deleted from the file.
How much time it takes, and why contributing to open source pays off
Contributing to someone else's project costs more than a local patch, and there's no point pretending otherwise. You have to reproduce the bug in isolation, write a test that fails before the fix and passes after it, adapt the code to the project's conventions and respond to review comments. Sometimes the fix is rejected because the authors have a different idea of how to solve the problem, and the work ends with a discussion in the issue.
The return comes later, and in less obvious places. Updating the library in the client project doesn't require reapplying patches. The next project using the same library gets the fixed version straight away. Knowing the library's internals shortens the hunt for the causes of future bugs. When security updates come out, the project is on the official version and can take the fix the same day.
Our own open tools work out differently. They cost documentation, handling issues from people outside the team, and release discipline. In return, we get tools tested on other people's setups, not just ours, and code we can show to anyone who asks how we work. For virtualisation testing we have a homelab, so infrastructure tools can be tried on real virtual machines, not just on the author's laptop.
Not everything pays off. A tool so tightly bound to an internal setup that nobody else could run it isn't fit to be opened. A public repository that can't be used is just an obligation to answer issues, with no value to anyone.
A separate case is a library abandoned by its authors: the last release several years ago, pull requests without a response, issues without a reaction. Sending a fix there is work that will never be accepted. In that situation there are two honest options: maintain your own fork deliberately, with someone responsible for it, or plan to replace the library with a maintained alternative. From the client's point of view, the second option is usually cheaper, even if it takes more work up front.
What a proper contribution looks like, step by step
If you lead a team of developers and want them to start sending fixes upstream to libraries, the order below saves both sides the most time. The order matters, because the most expensive mistake is a finished change that the library's authors first hear about from a ready-made pull request.
without the code of the project where you found it, ideally a dozen or so lines anyone can run.
the bug may already have been reported, fixed in a newer version or declared intended behaviour.
commit format, test requirements, licence agreements, rules for changes to the public API.
describe the problem and the proposed solution before you write code. The authors may have plans you don't know about.
one problem per pull request, a test that fails without the fix, a description that links to the issue.
a pull request abandoned after the first comment gets closed, and the next one from the same person is read with less trust.
The first step is the one most often skipped, and it pays off the most. A minimal example that reproduces the bug can show on its own that the problem lies in how we use the library, not in the library. Then, instead of a pull request, the fix goes into our own code and nobody wastes time on review.
The fourth step protects you from the most frustrating scenario: a week of work on a change the authors reject because they planned to rewrite that module differently. A short issue asking "would you accept a change like this" costs a few minutes.
Security bugs aren't reported in a public issue. A vulnerability description visible to everyone is an attack manual for every application that hasn't received the fix yet. Mature projects have a SECURITY.md file with a reporting address, or private vulnerability reporting enabled on GitHub. If the project has neither, write to the author privately and ask for a channel.
Once the fix is accepted, the work doesn't end when the pull request is closed. In the project where the bug was found, you have to update the library to the version with the fix, remove the temporary patch or the pin to the fork, and check that the tests pass on the official release. Skipping this step leaves a patch that lives for months alongside the fix in the library and sometimes applies the change a second time.
How to vet a vendor by their GitHub
If you're choosing a software house and want to look at its code, the points below tell you more than the number of repositories or stars on a profile.
- Commit history. Do changes arrive regularly and with descriptions, or does the repository have a few commits made on the same day, just before it was published?
- Tests and automated checks. Does the repository have tests, do they run on every change, and do the latest runs pass?
- Responses to issues. How does the author talk to people who report bugs: matter-of-factly, asking for details, or not at all?
- Contributions to other people's projects. A GitHub profile shows pull requests in other authors' repositories. A change accepted into a well-known library has passed review by someone who had no reason to let it through.
- README and documentation. Can you run the project just by reading the description, without asking the author?
- Dates. Are the projects maintained, or was the last change several years ago?
No public code doesn't disqualify a vendor. Many good teams work exclusively in clients' private repositories and have contracts that forbid publication. In that case, ask about access to the repository and test environment during the project. A vendor who won't give that even to a client paying for the code usually has a reason, and it's rarely a reason that's good for the client.
Contributions to other people's repositories are easiest to find with GitHub search. The query is:pr is:merged author:USERNAME -user:USERNAME shows that person's merged pull requests in repositories that don't belong to them. For an organisation, use -org:ORGANISATION-NAME instead of -user:. Open a few of them and read the discussion: you'll see how the vendor reacts to comments and whether the fix was anything more than a typo in the documentation.
Treat star counts with caution. A star is a single click, they can be collected through promotional campaigns, and a popular repository with a list of links has more of them than a solid library used in production. What says more is whether the repository has issues from people outside the author's team, because issues are written by people who actually use the code.
Our projects, numbers and activity are on the open source page. How access to the repository and staging works during a project is described on the how we work page. If you have a project that would benefit from someone who knows the libraries from the inside, write to us. We reply within an hour.



