Skip to main content

Cal-FOSS: OpenCal

Liberating a 470K-Line Codebase I Never Read

4 sessions42 prompts~2h over 6 days human~27h (inc. 70 sub-agents) AI
brownfieldcal-comopen-sourceforkplugin-architecturetypescriptsub-agents
View Live Project →
Human: ~2h (7%)AI: ~27h (93%)

The Setup​

I wanted to self-host calendar invites. Cal.com markets itself as open-source scheduling infrastructure, but when I tried to install it, I found what everyone in the community was already complaining about: the "open core" couldn't run without the proprietary parts. Two import aliases -- @calcom/features/ee/ and @calcom/ee/ -- threaded 1,011 proprietary references through 543 files. No feature flags, no conditional loading. Static imports baked into the FOSS code so deeply that removing ee/ broke 187 test files immediately.

The codebase was 470,000 lines of TypeScript across 26,000 commits. 6.2 gigabytes on disk. I never read the source code. Not a single file. Every decision I made was based on surveys, import analysis, and reports that Claude produced -- the same way I'd manage a team of engineers on a system too large for any one person to hold in their head.

This is a story about operating at scale you can't see, and the pivot that turned demolition into architecture.

Activity During the Build​

OpenCal was a side project — I was running 3-7 other projects simultaneously throughout the week. The highlighted segments show when I was actively prompting the cal-foss sessions; everything else was other work.

07:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:00
Mar 115.1h
Mar 126.6h
Mar 134.5h
Mar 162.5h
Mar 174.8h
Mar 183.0h
OpenCal
Undisclosed 1
Undisclosed 2
Undisclosed 3
Undisclosed 4
Undisclosed 5
Undisclosed 6
Undisclosed 7

Stage 1: Survey and Demolition — Mar 11-12 | Session 1 | 52 prompts | Entanglement mapping, EE removal, phone-home discovery, architectural pivot

Opening Probe​

I started deliberately vague. I wanted to see if Claude could confirm what I already suspected.

Session
Opening prompt -- testing Claude's awareness of the problem
>there's a project called cal.com, it's supposed to have a FOSS core but somehow the company maintaining it did some games with the code and license where it's basically impossible to run it self-hosted without including the non-foss license stuff, check if you can figure out what I'm talking about.

Claude confirmed the entanglement: 938 EE import references across 543 files via one alias, 73 more through a second. Unconditional static imports, no feature flags. I dispatched three sub-agents simultaneously to survey the full scope -- one for apps/web/ EE imports, one for core packages, one for internal EE cross-dependencies. The surveys came back with a comprehensive entanglement map: organizations (45+ files), workflows (40+), teams (35+), billing (20+).

The Blunt Start​

Session
First surgical move
>rm -rf ee, commit.
>before we remove EE, get tests running. then remove them, then commit. If tests somehow run after removing ee let's track what the list of failures are in a context file.

187 test files broke. I directed creation of MIT-licensed replacement stubs in a new packages/mit/ directory, explicitly insisting we check our replacements didn't accidentally mimic EE code. Stubs fell into three categories: always-say-yes gates (license checks that just return true), do-nothing no-ops (functions that accept calls and do nothing), and safe-default returns (functions that return empty arrays or neutral values).

The Phone-Home​

During the survey, Claude found something I didn't even notice at the time. A Prisma extension at packages/ee/prisma-extensions/usage-tracking.ts was silently reporting every booking and user creation to goblin.cal.com via HMAC-signed requests. For software marketed as self-hostable, this undisclosed telemetry was exactly the kind of thing I'd told Claude to watch for in my project rules. It detected and removed it autonomously -- replaced with a no-op, blanked the URL from constants.

The Pivot​

Through the session I'd been removing caller-side wiring -- about 977 lines of code that called into EE modules. Tests were converging (504 of 566 passing). Then I stopped and asked a question that changed the project.

Session
The moment of architectural pivot
>how much of the stuff we deleted in previous commits should have gone into plug-ins?
>ok let's zoom out big. Assume we rm-rf all /ee/ code. Assume we don't need licenses any more because everything will be FOSS. assume we will not actually implement a feature but just leave not-implemented stubs that can be replaced via plug-ins.
>we're mapping right now, not coding.

The 977 lines I'd deleted were the integration points. They were exactly where a plugin system would hook in. I'd been removing the wiring that future extensibility needed. The right move was to revert, preserve those connection surfaces, and redesign them as plugin slots.

I know this architecture from previous experience -- extension points, registry patterns, plugin contracts. But I didn't plan it from the start. I planned to just strip out EE. It was seeing what the stubs looked like after removal that made me realize the EE modules probably did something useful to other people, and the integration points could stay if they were restructured around concepts that change together instead of a monolithic "EE" tie-down.

Five research agents were dispatched simultaneously to map the plugin surfaces: SSO/SAML/DSYNC, round-robin/CRM/managed-events, workflows, billing, and other feature domains.

Stage 2: Plugin Architecture — Mar 13 | Session 2 | 23 prompts | Extension point mechanism, 7 plugin domains, 315 type errors driven to zero

The Implementation Plan​

I opened by pasting a multi-phase implementation plan: revert the caller-side wiring deletions, build the extension skeleton, implement 7 plugins in parallel. This plan came from a prior Claude conversation -- I'd mapped the problem in Session 1, thought about it overnight, and arrived with the architecture ready.

Session
Directing parallel execution
>start a sub-agent to inventory 4. document it. report back when done. are any of the safe defaults interesting extension points in the future? start a sub worker to do item 2. sounds easy, no?

The core mechanism was createExtensionPoint<T>() -- a factory pattern in packages/mit/extensions/_registry.ts providing a simple { get(), register() } interface. Seven plugin directories were created: workflows, billing, sso, round-robin, managed-event-types, crm, and attribute-sync. Each with interfaces, defaults, and a container. A cal-foss.config.ts at the project root served as the plugin registration entry point.

Driving Type Errors to Zero​

315 type errors. Eight parallel agents worked across different domains -- workflows (the largest, 25+ stub files), billing (Stripe types), SSO (SAML controller stubs), teams (10+ missing repository methods). They worked in waves: first missing exports and methods, then type mismatches, then parameter signatures.

Session
Terse management
>update context files. fix the 1 stale ee ref. run tests, fix as needed — ask if it is weird. commit the work. do phase 1 later on.

The test categorization agent separated failures into three buckets: 10 pre-existing environment failures (not our problem), 16 expected FOSS behavior changes (correct -- these tests asserted EE behavior), and only 2 tests actually broken by our changes.

The fork got its name this session: OpenCal.

Stage 3: Code Extraction — Mar 16 | Session 3 | 3 prompts | Moving production logic out of stub layer

A focused session. Some files in packages/mit/ contained real production logic -- Prisma queries, crypto operations, URL construction -- that didn't belong alongside no-op stubs. I pasted the extraction plan and Claude moved 11 files to their proper locations in packages/features/ and packages/lib/, updating imports across 168 files.

The approach was smallest-surface-first: API keys (2 importers), then teams (21), then organizations (33 importers touching 60+ files). Each domain: git mv, bulk import update, type-check verification.

The type-checker hit OOM errors on the 6.2GB codebase -- NODE_OPTIONS="--max-old-space-size=8192" became a permanent fixture.

Stage 4: Test Fixes and Build — Mar 16-18 | Session 4 | Test assertions, Next.js build gap, final fixes

Parallel Test Agents​

Three agents dispatched simultaneously for test fix groups. Agent 1 fixed 47 tests across 6 files, Agent 2 fixed 26 across 7, Agent 3 fixed 8 across 2. All 15 Category B test files -- tests that asserted EE behavior against what were now FOSS stubs -- updated in parallel.

The Build Gap​

The Next.js production build revealed what the type-checker missed. Dozens of missing stub exports that TypeScript's project-reference resolution hadn't caught because Next.js does stricter resolution at build time. A long-running agent spent nearly 7 hours fixing 40 missing exports. A second agent ran 20+ hours handling cascading consumer-side type errors.

This was the gap between "it type-checks" and "it builds" -- and on a 470K-line codebase, that gap was wide.

What This Shows​

Operating at Scale You Cannot See

470,000 lines, 26,000 commits, 6.2 gigabytes. I never opened a source file. Every decision -- which modules to remove, where cascade failures would propagate, what the plugin interfaces should look like -- was based on Claude's surveys and import analysis. This is how I'd manage any project too large for one person: dispatch scouts, read their reports, make architectural calls. The AI didn't change the management pattern. It changed who I could dispatch.

The Pivot Pattern

I started with demolition: strip EE, stub the imports, make it compile. Forty-five messages in, I realized I was destroying the integration points that a plugin system would need. The pivot -- revert everything, redesign those connection surfaces as extension slots -- came from seeing the shape of the problem after doing the wrong thing first. The "wrong" work wasn't wasted. The surveys, the stub inventory, the entanglement map -- all of it fed the plugin architecture. Sometimes you have to do the surgery to discover what the patient actually needs.

Sub-Agents as a Team

70 sub-agent sessions across 4 main sessions. I dispatched them the way I'd organize a team of engineers: three scouts mapping EE imports in parallel, eight agents fixing type errors across different domains, four agents fixing test groups simultaneously. I used Anthropic's agent teams coordination, which creates a shared file for agents to communicate through and avoid conflicts. This isn't a new skill -- it's the same approach I used managing human teams on large projects. Map the problem, develop a strategy, execute in parallel, iterate when the ground reality surfaces nuanced problems.

Tight Coupling in Open-Core Codebases

Cal.com's architecture uses 938 unconditional imports across 543 files, no feature flags, no conditional loading, and a second import alias adding 73 more references. The result is that removing the proprietary code breaks the open-source code -- something many in the community have noted. The plugin architecture inverts this: extension points are explicit, opt-in, and MIT-licensed. The same integration surfaces exist, but the coupling is under the user's control.