Logo
Logo
  • Home
  • About
  • Services
    • Fractional CTO
    • Application Architecture
    • Platform Audits
    • Platform Rebuilds
  • Projects
  • Writing
  • Work
  • Request Quote
Logo

Backed by 20+ years of hands-on software development expertise, mithra62 transforms ideas into powerful, reliable solutions—designed to work exactly how you want, every time.

  • Address

    Tucson, AZ 85712
  • Email

    eric@mithra62.com
  • Contact

    +1-310-739-3322

Done Happens Offstage

  • Home
  • Writing
Done Happens Offstage
05 Aug 26
  • Programming
  • Rant
  • AdAstra

I've got another one about to go out the door. Tests green, package resolves, docs written, the whole ritual. And standing here with my finger over the release button, I realized I know exactly how to finish software and I have almost no idea what "done" means.

Those aren't the same thing, and it took me a looooong time to notice.

When you release something you already have the success criteria loaded. People install it. Somebody builds a thing on top of it. A few developers file pull requests. An ecosystem shows up. Maybe, eventually, somebody pays you money. That's the list. It's a reasonable list. I've written that list in my head before shipping, more than once, and every time it felt like a description of the future.

Here's the problem. I've now been around long enough to look back at the projects I shipped with that exact list in my head, and the outcomes that actually mattered weren't on it. Not "we hit the list late" or "we hit a smaller version of the list." The list was just wrong. The projects went and became useful somewhere I never pointed them.

So I want to walk through two of them, because they're the evidence, and then come back to the button I'm still not pressing.

EE Objects, or: this is obviously how everyone should write EE code

Go back to ExpressionEngine and working with Channel Entries programmatically. If you were there, you can probably feel the tension in your shoulders.

The custom field storage was field_id_XX. The member field was m_field_id_YY. Your application code, the code that was supposed to be about members and profiles and permissions, knew those column names. It had to. To get a member's first name you reached into the storage layer and pulled m_field_id_17 because that's where first name lived that week, on that install, until somebody reordered the fields and it lived somewhere else. Your domain logic was fluent in a database dialect it should never have had to learn.

EE Objects put a layer over that. The Member, Channel, and Category objects meant that instead of your code knowing first name was m_field_id_17, your code said:

$member->get('first_name');

The application got to speak the language of the application. The storage got to keep its ugly little secrets to itself. That was the pitch for that piece, and I still think it was a good pitch.

But EE Objects was never just that piece. It was a framework. There was a template parser under it, the thing that made "render this template and send it as an email" work. There was an add-on abstraction so you weren't rewriting the same add-on scaffolding every time. There was a shared form layer and a set of validation rules so every add-on didn't reinvent form handling and then get it subtly wrong. The Member and Channel objects were the vivid part, the part with the obvious before-and-after, but they were one wing of a larger building.

I didn't treat any of it like a script I kept in a gist. It was a Composer package on Packagist, it had unit tests, it had tagged releases, it had documentation and a public API I actually thought about. I built it for other people to composer require and lean on. I knew what done looked like. Done looked like developers adopting it.

Developers did not adopt it.

EE Objects doesn't do that

I want to let that sit as a failure for a second before I rescue it, because the rescue is the part that fooled me for quite a few years.

EE Objects didn't become the abstraction layer the ExpressionEngine world reorganized itself around. People didn't restructure their add-ons to speak in domain objects. By the definition I picked on release day, the one where developers adopt the library, it flopped. The install count was the install count. I can be honest about that now.

But the ideas didn't die with the adoption numbers. They walked off on their own. The add-on abstraction landed in CartThrob first, which was the proof it worked in the wild and not just in my head, an actual shipping product built on the scaffolding. So I wrote it up as a pull request against ExpressionEngine itself. It got accepted. Today every EE add-on is built on that design. The shared form layer went the same direction, up into the platform.

And here's the inversion I didn't see coming. I wanted developers to adopt the library. Instead the software adopted the ideas, one component at a time, and never the whole thing at once. The pieces I'd have bet least on, the add-on plumbing nobody demos, turned out to be the pieces that traveled the farthest, all the way into the core. The abstractions I shipped got quietly obsoleted by the thing they were built to improve, because the thing they were improving absorbed the lessons and stopped needing a separate package to hold them.

The project succeeded, if it did, by making itself unnecessary. It dissolved upward into the platform.

Was EE Objects done? I guess. That's the honest answer and it's an uncomfortable one, because I can't point to the moment, and I definitely can't take credit for engineering it.

JaegerApp, or: fine, let's abstract the whole CMS

If EE Objects asked why application code should know how ExpressionEngine stores a field, JaegerApp asked the bigger, dumber, more ambitious version of the same question. Why should the application know which CMS is underneath it at all?

The idea was to build applications that ran across the major PHP content systems of the era by abstracting the parts they all had and papering over the parts where they disagreed. Database access, a REST layer, shared services, and a set of Platform objects so the same application logic could sit on top of different systems without caring which one it landed on. And I didn't mean two or three. There were Platform abstractions for Drupal 8, ExpressionEngine 2 and 3, Craft 1 and 2, WordPress, Concrete5, Magento, and PrestaShop (plus a Console target, so any of them could share a unified console layer, or so you could write a standalone PHP console program with nothing underneath at all).

This wasn't a whiteboard fantasy either. Backup Pro was the proof, and it ran on every one of them. The same product, the same application logic, sitting on Drupal and WordPress and Magento and the rest through Jaeger; the abstraction carried real weight for a real product people paid for. And Jaeger got the full treatment, same as before. Composer, Packagist, unit tests, releases, a public package built so other developers could build portable things on top of it.

Same ritual, same confidence, bigger swing. I knew what done looked like. Done looked like developers using Jaeger to write CMS-agnostic applications.

I'll be honest about the swing while I'm here, because I could already feel the survivorship story forming and I want to head it off. At the time I could not have told you whether the architecture was genuinely prescient or whether I'd built an elaborate abstraction because abstracting things is fun and I wanted an excuse. You mostly can't tell those apart in the moment. You find out later, and "later" does the deciding without asking your opinion.

And then a business ruins the plan

Jaeger did not conquer the PHP CMS ecosystem. Something else happened, and the something else was that VITY started working.

Then VITY started really working. And a product that's working reorganizes your priorities whether you consented to the reorganization or not. The CMS add-on world got a lot less interesting the moment I had an actual business standing in front of me asking for attention. I drifted out of CMS development to concentrate on the thing paying for itself.

JaegerApp, the open-source cross-CMS framework, got abandoned. I'm not going to dress that up. I abandoned it. By the original definition of done, developers building portable applications on Jaeger, that's about as conclusive a failure as you can file.

Except Jaeger came with me.

Except Jaeger followed me home

VITY started life bolted tight to ExpressionEngine, the way everything I built back then started. But Jaeger had already cut seams between the application logic and the CMS, seams I'd cut for a reason that turned out to be the wrong reason, and those seams were exactly where things came apart cleanly when I needed them to.

It happened as a slow migration, not a decision. The database functionality moved out of the CMS. The REST functionality moved out. The core services moved out. One by one the application stopped reaching into ExpressionEngine and started standing on its own, until ExpressionEngine wasn't the application anymore; it was just the thing that booted the application and handled integration at the edges.

Bit by bit, VITY stopped being an ExpressionEngine app. Eventually what was left of the EE-specific code was a couple thousand lines of bootstrap and glue. The actual core could stand up more or less on its own.

Jaeger never became the cross-CMS framework I shipped it to be. Nobody's out there writing platform-agnostic applications on it. What it became instead was the set of seams that let me pull a CMS out of the product that actually mattered to me. That was not on release day's list. It was worth more than everything that was.

Notice this is a different move than EE Objects made. EE Objects dissolved upward; its ideas got absorbed into the platform until the library was redundant. Jaeger did the opposite. It didn't dissolve, it emigrated. The architecture outlived the framework and went to work somewhere else entirely. Two projects, two failures against their own success criteria, two completely different ways of being useful anyway. One got eaten. The other escaped.

So which one failed?

By the definitions I chose on release day: both. Neither hit the adoption I imagined. If you'd graded them against the outcomes I imagined on release day, you'd have marked both failures and you'd have been right.

And this is where I finally have to admit the two words I'd been using interchangeably are not the same word.

I always knew when the software was complete. Complete is knowable. Tests pass. The package resolves. Releases are tagged. The docs exist. Packagist can install the thing and the thing does what the README says it does. Complete is an engineering fact about an artifact, and I'm good at engineering facts. I can tell you to the commit when something is complete.

What I could not tell you, either time, was when the project was done. PHPUnit tells you whether the code works as expected. Packagist tells you published. GitHub's insights tab tells you adopted. Not one of them tells you done, because done wasn't a fact about the software. Done was a fact about me: about where my attention went, what the project quietly turned into, whether it was even still mine to finish. Both projects reached done offstage, years after release, without asking my permission and without me noticing until I looked back.

The part where I don't get to keep the nice story

Here's the guardrail, because everything above is one confident retelling away from founder-brain survivorship garbage.

Not every abandoned repo is secretly a triumph. Most abandoned repos are just abandoned. Sometimes you misread the market. Sometimes the abstraction is genuinely wrong, not wrong-but-secretly-load-bearing, just wrong. Sometimes nobody needs the thing. Sometimes somebody solves the problem better and you should be glad they did. Sometimes the code isn't very good and the reason it didn't get adopted is that it didn't deserve to. A repo nobody uses is, the overwhelming majority of the time, a repo nobody uses, and no amount of narrative will resurrect it into a case study.

So the lesson is not "everything works out." The lesson is narrower and less comforting. You don't know at release time what the useful life of the thing is going to be. You get to know when it's complete. You get to have ambitions for what happens next. You do not get to know when it's done, and pretending you do is how you end up disappointed by projects that were, by some other measure you hadn't invented yet, doing fine.

Release doesn't guarantee the software becomes useful. It just gives it the chance to.

Back at the button

Which brings me back to the thing waiting to ship, and the fact that I'm doing every part of the ritual again, fully aware of the ritual.

I want people to use it. Of course I do; I wouldn't be shipping it otherwise, and anyone who tells you they release software without wanting it used is either lying or has never done it. I've got the list in my head right now. Installs, builds, contributors, an ecosystem, maybe revenue someday. Same list.

The difference is I've got enough history now to know the list is a guess. Maybe it becomes exactly what I'm building. Maybe one piece of it turns out to matter more than the whole. Maybe it becomes the plumbing for something I haven't thought of. Maybe nobody touches it and it's just a repo nobody uses, and I'll have to sit with that being an actual possibility and not a dramatic setup for a comeback. I don't know. I've stopped needing to know before I ship, because the two projects that taught me this both went and found their real purpose in a room I wasn't standing in.

EE Objects was supposed to be done when developers adopted it. It got done when the platform swallowed the idea and made the library pointless. Jaeger was supposed to be done when developers built on it. It got done when it pried a CMS off the back of the thing I actually cared about. Neither of those was on the release-day list. Both were worth more than the list.

I can tell you when software is complete. I've done it enough times to be good at that. When it's done is a question the software gets to answer later, on its own schedule, and the most useful thing I've learned in twenty years of shipping is to stop pretending I get a vote.

Built something that found a completely different purpose after you released it? I'd love to hear where it ended up.

Get Started

Recent Post

  • Done Happens Offstage
    Done Happens Offstage
    05 Aug, 2026
  • Confidence Doesn't Need an Audience
    Confidence Doesn’t Need an Audience
    29 Jul, 2026
  • The Greatest Hits of ExpressionEngine History
    The Greatest Hits of ExpressionEngine History
    22 Jul, 2026

follow us

Logo

Backed by 20+ years of hands-on software development expertise, mithra62 (Eric Lamb) transforms ideas into powerful, reliable solutions designed to work exactly how you want, every time.

© Copyright 2026 | mithra62

Useful Links

  • About
  • Projects
  • Writing
  • Work
  • Request Quote

Services

  • Fractional CTO
  • Application Architecture
  • Platform Audits
  • Platform Rebuilds

Contact Info

Get in touch now to begin work immediately.

  • Email: eric@mithra62.com
  • Contact: 310.739.3322