DEV Community

Cover image for What Replacing Calendly Taught Me About Trusting Open Source

What Replacing Calendly Taught Me About Trusting Open Source

Pascal CESCATO on July 29, 2026

cal.com, Calendly, zcal... booking SaaS isn't short on options, and most of them are genuinely decent. Free tiers cover the basics for a lot of fre...
Collapse
 
nazar-boyko profile image
Nazar Boyko

On the MySQL side you might get away without GET_LOCK for the common case. Appointment slots come off a fixed grid rather than arbitrary ranges, so a unique index on (id_users_provider, start_datetime) makes the second insert fail on its own and you turn the duplicate key error into the same "unavailable" message. Doesn't help with the ANY_PROVIDER search, but there's no lock lifetime to reason about either.

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

That's a good point. For a single provider, a UNIQUE constraint is indeed a much simpler solution and lets the database enforce consistency naturally.

The tricky part was the ANY_PROVIDER workflow, where the application first has to decide which provider gets the slot before inserting the appointment. That's where the race condition appeared and why I ended up looking at explicit locking.

Collapse
 
wrobeltomasz profile image
Tomasz

By migrating customer data to your own infrastructure, you assume full responsibility for its security and compliance with the GDPR. How do you handle backups, monitoring, and a potential DDoS attack on your instance?

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

That's a fair point. Self-hosting doesn't remove responsibility, it moves it.

The trade-off is that I now control the infrastructure, the update cycle, and the data flow instead of delegating everything to a SaaS provider with limited visibility.

For this kind of application, I would treat it like any other business system: automated backups with restore tests, monitoring, restricted access, security updates, and a layer in front of the instance for common attacks.

The interesting question is not whether self-hosting is risk-free (it isn't), but whether the added responsibility is worth the additional control and transparency.

Collapse
 
manutopik profile image
Emmanuel Salomon

Try cal.rs

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

Interesting… I'll try it.

Collapse
 
shweta_mishra_b3c97874de9 profile image
Shweta Mishra

Interesting perspective. I think one of the biggest advantages of open source isn't just avoiding vendor lock-in—it's understanding the system well enough to adapt it as your requirements evolve. That said, replacing a mature SaaS also means taking ownership of reliability, security, and long-term maintenance. Curious to know what the biggest unexpected challenge was after the migration.

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

The biggest surprise wasn't the migration itself—it was discovering that the real challenge wasn't replacing the SaaS, but validating the assumptions behind the open-source alternative.

I expected configuration and feature gaps. I didn't expect to spend so much time auditing concurrency logic and finding a race condition that had apparently gone unnoticed for years.

In the end, the migration became much more of a code review exercise than a deployment exercise.

Collapse
 
mudassirworks profile image
Mudassir Khan

the cal.diy detail is the most interesting part — a fork that exists because the upstream project's threat model changed (AI assisted scanning against a public repo), not because the code went bad. that's a new class of open source trust failure: the license stays open but the maintainer's incentives stop aligning with community use.

finding the TOCTOU race in both finalists after all the UX deliberation is the actual argument of the post. the "trust open source" conclusion earns its weight because you did the work that most devs skip.

curious whether Easy!Appointments has acknowledged the race condition or if the fix ended up being your own patch?

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

Thanks! That was really the point I wanted to make: open source isn't about assuming the code is bug-free, it's about having the opportunity to verify the assumptions yourself.

As for Easy!Appointments, I haven't submitted a patch. For my own use case, the probability of hitting that race condition is extremely low—I'm expecting around a hundred appointments per month, spread over time—so the practical risk is close to zero.

If I were running a high-volume public booking service, I'd definitely revisit that decision. But for my deployment, understanding the limitation was more important than eliminating a corner case that is unlikely to occur in practice.

Collapse
 
talha_ramzan_3878156fea8c profile image
Talha Ramzan

The "documented intent, not what the code actually does" line on Easy!Appointments is the part that really lands. A docblock confidently stating "the app won't allow this to happen" sitting right next to code that does nothing to enforce it is exactly the kind of gap that survives ten years of production traffic, because nobody goes looking for a bug the comments already told them doesn't exist.

The fact that the correct primitive (has_provider_conflict) already existed in the codebase but was never wired into the actual booking flow is almost more unsettling than if the logic had just been missing entirely, it means someone built the right fix at some point, and it still didn't make it into the path that mattered.

Respect for the closing honesty too. Most writeups like this end on "and here's the fix, problem solved," but stating plainly that your own deployment is still exposed to the exact race window you just spent the whole article documenting is a rare thing to admit publicly. Curious whether you're leaning toward the per-provider GET_LOCK approach as the eventual fix, or reconsidering the non-persistent-connection assumption first, since that seems like the part most likely to bite you quietly in a shared hosting environment.

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

The has_provider_conflict point bothered me for exactly the same reason. Finding missing logic is one thing; finding logic that exists but is disconnected from the path where the decision actually happens is much harder to spot.

Regarding the fix, I'm still leaning toward solving the concurrency at the database boundary rather than relying on application-level assumptions. The per-provider GET_LOCK() approach is attractive because it matches the actual resource being protected, but you're right that the connection lifetime assumption deserves more investigation, especially outside controlled environments.

The interesting lesson for me is that the race was not caused by a missing feature. It was caused by a gap between the model the code claimed to implement and the execution path that actually ran.

Collapse
 
prahladyeri profile image
Prahlad Yeri • Edited

Forget 'Infrastructure I control instead of third party's'. The world is now moving towards offline-first and local-first approaches, architect the web app in such a way that the most critical data won't leave your computer's shore in the first place, except for syncs and backups.

The presently prevalent cloud server (or client server) paradigm is a vestige of an era when most browsers used to be 'thin clients' and lacked capabilities of compute and storage. But this is no longer a case today and better approaches are possible.

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

I think that's a different discussion.

My goal wasn't to design a new scheduling application or rethink web architecture from scratch. It was to replace a proprietary SaaS with an existing open-source solution that I could deploy today.

A local-first scheduling application would certainly be an interesting project, but it would require building a completely different product, which is outside the scope of this article.

Collapse
 
xulingfeng profile image
xulingfeng

Solid piece. Makes me wonder though — in a world where AI writes more code every day, are we quietly losing the ability to actually read it? That TOCTOU gap didn't surface because the project was mature. It surfaced because you sat down and read the source. That's the kind of thing that stops mattering once everyone stops looking.

Collapse
 
tom_jones_230c4659491adcd profile image
Tom Jones

The TOCTOU find is the interesting part, and I want to put a number against your question, because we measured the thing people reach for once they stop reading.

The usual fallback is "have a second model check it." We tested what that is worth on our own serving path: two different models, serve the cheap answer when they agree. On code, with executable tests as ground truth, the gate still lets through 1.7 to 3.5 percent wrong. On faithfulness judgement, with nothing to execute, agreement approves a wrong answer 27.5 percent of the time, 95 percent interval 16.1 to 42.8. So agreement is a decent cost lever and it is not an audit.

A race condition is close to the worst case for it. Two checkers only substitute for a reader if their errors are independent, and a concurrency bug is invisible in a single-threaded read of the source, so two readers with the same habit miss it together. Shared structure in the input is a correlation source, which is why agreement tends to look strongest exactly where it deserves the least trust.

What would have caught this one mechanically is not a better reader, it is a test that executes: fire two concurrent requests at the same slot and assert that exactly one wins. That keeps working after everyone stops looking, which is the part your question is really about. The reading found it once. The test finds it every time.

One trap on writing that test, straight out of the post above: SQLite serialises writers, so the race cannot reproduce there. The test passes vacuously and the bug ships anyway on Postgres or MySQL. It has to run on the engine you actually deploy on. We learned that expensively this month, on a change that was green in staging and took our cheap model tier off in production, because staging could not exercise the failing path at all.

Collapse
 
xulingfeng profile image
xulingfeng

Years in QA here — "run it on the engine you actually deploy on" hit hard. Lost count of how many times staging was all green and production went up in flames a few hours later. Appreciate you sharing those numbers. More testing is always better in theory, but time and cost have a say too. Finding that balance — or a decent enough one — is kind of an art.

Thread Thread
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

That's exactly why I included the TOCTOU example. Finding the bug was valuable, but turning it into a regression test is what makes the discovery useful over time. Otherwise, it's just tribal knowledge waiting to be forgotten.

Thread Thread
 
tom_jones_230c4659491adcd profile image
Tom Jones

Both of those land, and they meet at a filter that is free to apply, so it is worth naming.

On the balance question: it is judgement in general, and there is one case where it is not, and it is the case in this post. A SQLite test for a race that only exists under concurrent writers is not a weaker test. It has no power at all, because the engine serialises the writers and removes the failure mode from the universe. The test passes by being unable to fail. So before the time and cost question there is a cheaper yes or no: can the environment I am testing in physically produce this failure? If the answer is no, running it is worse than not running it, because you have replaced a known unknown with a green check.

That is also the sharp edge of the tribal-knowledge point. A regression test only converts the discovery into something durable if it can still fail. Ours could not, and we did not notice for weeks. Four of our routing safety suites were passing while a helper we mock had gained a fourth return value and our fake had not, so the suites were exercising an object that no longer resembled the real one. Same disease as the SQLite case arrived at from the opposite direction: the knowledge had been written down, the test was green, and the protection was gone.

So the version of your rule I would carry is that a regression test needs a second act after it is written. Break the thing it guards and prove the test goes red. We do that now, and it caught a suite where the asserted string appeared at two call sites, so breaking one of them was invisible and only breaking both turned it red. Without that step you have converted tribal knowledge into a green check, which is the more expensive of the two, because a green check stops anyone from looking.

Everything after that filter really is judgement and cost, and I do not think there is a formula. But the filter is free, and it tends to delete a couple of tests people feel guilty about skipping and promote one or two they were not thinking about.

Thread Thread
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

That's exactly where my skepticism comes from.

I don't distrust tests themselves, I distrust the confidence people sometimes attach to them. A green test suite can mean "the system is protected", but it can also mean "we successfully tested the assumptions we made when writing the tests".

The production environment remains the ultimate judge because it introduces the combinations nobody thought about: real data, real users, real timing, real failures.

For me, the value of a regression test is not that it exists, but that it represents a failure that actually happened and that we proved it can fail when the condition comes back. Otherwise, it is just another artifact that creates confidence without necessarily creating safety.

Some comments have been hidden by the post's author - find out more