Skip to content
Thomas BerdyThomas Berdy
July 15, 2026 · 6 min read

Our Auth Library's Last Release Was July 2022. We Noticed in June 2026.

Our Auth Library's Last Release Was July 2022. We Noticed in June 2026.

A dead library ran our dashboard's authentication for four years without a single alert, because dependency bots only report new versions and known CVEs.

I was reviewing a teammate’s PR against our dashboard’s authentication code when I hit an import I’d never seen: imia. I got curious and opened the upstream repo. It was archived, and its last release dated back to July 2022.

imia ran session authentication for the Mergify dashboard: every login and every staff impersonation. We adopted it in June 2022, when it looked alive; its final release shipped a few weeks later. It held that job for four years while upstream sat still, and nothing in our tooling ever said a word.

Nothing warned us

Our bots watch dependencies all day, but everything they show us is one of two things: a new version to bump to, or a published vulnerability to patch. An archived project rarely produces either. There’s no release to bump to, and if a vulnerability turns up, there’s no maintainer to write the fix. Your bot never gets a patched version to offer you. The pin keeps resolving and CI stays green, so the silence reads as health.

When I asked myself how long we would have kept shipping on top of it without that lucky code review, the honest answer was months, maybe years.

The impact analysis I wrote that morning wasn’t dramatic, which is what makes this failure mode nasty. The pinned wheel still installs. Nothing breaks tomorrow. The costs are all deferred: a security issue in our auth layer would go unreported and unfixed, and imia 0.5.3 only keeps working with current Starlette because it sticks to Starlette APIs that have stayed stable so far, so a Starlette release would eventually pick the migration deadline, not us.

There’s a fair objection that a small library over stable APIs might be finished rather than abandoned, and for a string-padding helper I’d accept it. The layer that decides who is logged in doesn’t get that benefit of the doubt.

The fix I refused

The first version of our migration ticket proposed vendoring imia. It’s small (about 680 lines) and MIT-licensed, so copying it into our tree was the path of least resistance.

I killed that plan. Authentication is one of the most common building blocks in software, and the community maintains good libraries for it; owning a private copy of auth code that nobody else reads or patches felt like the worst of both worlds. Forking it publicly would have been the same commitment with extra ceremony. Vendoring can make sense for a leaf utility. For the code that decides who is logged in, I want as much shared, maintained code under it as possible.

So we listed the real options:

OptionWhy we passed
fastapi-usersWants to own the user model and ships password machinery we’d never use (our login is GitHub OAuth only). Our Redis-backed session flow would end up as custom strategies anyway.
AuthXJWT-based. Instant revocation means bolting a token blocklist back on, which is the server-side state our Redis sessions already give us.
Starlette-Login15 stars, one maintainer. That’s how we got into this situation.
Pin and monitorKeeps every risk and hands the migration date to a future Starlette release.

We picked starlette.authentication, the auth layer Starlette itself ships, plus our own adapters on top.

Which, if you squint, is exactly what imia was: thin glue over Starlette primitives. The custom code just moved down a level, onto the framework we already pin and track. It shrank to a couple hundred typed, tested lines that we own.

Swapping auth middleware without logging anyone out

A middleware-stack swap doesn’t realistically fit behind a feature flag: the middleware is either in the stack handling every request or it isn’t. Safety had to come from how we designed the change.

The decision that mattered most: keep the session storage exactly as it was. Sessions live in Redis via starsessions (same author as imia, but still maintained), and the new backend reads the same session keys imia wrote. That bought us what a feature flag or a dual-stack dispatcher would have: every session opened before the deploy stayed valid after it, and rolling back would have been an ordinary redeploy against the same sessions. Nobody got logged out, and nobody even noticed.

The swap turned out to be a simplification too. imia needed two middlewares chained together, one for authentication and one for impersonation. Starlette’s built-in AuthenticationMiddleware takes a backend, and ours resolves both in one pass: load the user from the session, then swap in the impersonated user when a staff member is driving.

Before: two imia middlewares chained

flowchart LR
    A1[Request] --> B1[imia AuthenticationMiddleware]
    B1 --> C1[imia ImpersonationMiddleware]
    C1 --> D1[Dashboard app]

After: one Starlette middleware, our backend

flowchart LR
    A2[Request] --> B2[starlette AuthenticationMiddleware]
    B2 --> C2["Mergify backend<br/>session user + impersonation in one pass"]
    C2 --> D2[Dashboard app]

The rest was mechanical: port the existing auth tests to the new backend, then walk through login, logout, and entering and exiting impersonation on staging. Our own PR-risk bot flagged the change as HIGH (about 1,200 lines across 17 files, touching the API layer), which felt fair for an auth swap. It merged three days after I first saw the archive banner.

Making luck less necessary

Finding this through idle curiosity in a code review is not a process. The frustrating part is that the data to catch it was sitting in public APIs: the registries have exposed the stale release dates since 2022, GitHub exposes an archived flag, and OpenSSF Scorecard even scores maintenance health from signals like these. None of that was wired into anything we actually look at.

So the same morning, I opened a second ticket: an auditor that walks our direct dependencies and pulls those signals into a ranked report. Archived or deprecated projects come first, then stale-but-alive ones. A single-maintainer project sitting on a sensitive path like auth or session handling gets extra weight.

That rule cuts both ways: starsessions, the same author’s session library we kept, sits on that watchlist. It isn’t abandoned, and this time something is watching instead of waiting for luck.

The first run flagged a handful of other dependencies that need care. We now have a ranked list instead of an accident.

What it can’t measure is the thing I actually want to know. Registries record versions and vulnerabilities, and GitHub records activity, but there’s nowhere a maintainer can say how much time they still have for a project. Every check we run (release recency, commit dates, archived flags, deprecation banners) is a proxy for one person’s available attention. imia’s author did the responsible thing by archiving the repo, loudly and unambiguously. Plenty of dying projects never get that. They go quiet, and your bots keep telling you everything is fine.

Go check yours. It takes five minutes to grep your lockfile for the libraries sitting under your login flow and open their repos. I found four years of silence under ours.

Stay ahead in CI/CD

Blog posts, release news, and automation tips straight in your inbox.

Recommended posts

We Stopped Our Config Builder From Destroying Your YAML, Then Found a Worse Bug Next to It
July 29, 2026·8 min read

We Stopped Our Config Builder From Destroying Your YAML, Then Found a Worse Bug Next to It

Our no-code config builder used to strip comments and anchors out of your .mergify.yml on every commit. Fixing it meant patching the document in place instead of re-dumping it, and an audit of the code next door turned up a parser bug that had been silently inverting a security condition in production.

Alexandre GaubertAlexandre Gaubert
The Native TypeScript Compiler Cut Our Typecheck From 13s to 3.5s. The Only Hard Part Was ESLint.
July 27, 2026·7 min read

The Native TypeScript Compiler Cut Our Typecheck From 13s to 3.5s. The Only Hard Part Was ESLint.

We swapped in TypeScript 7's native Go compiler and our dashboard typecheck went from 13s to 3.5s. The compiler was nearly a drop-in. The real work was keeping typescript-eslint alive when native 7.0 ships no importable compiler API.

Thomas BerdyThomas Berdy
Our Frontend Was Guessing What Our Parser Accepts. It Was Wrong for 70 of 75 Attributes.
July 17, 2026·8 min read

Our Frontend Was Guessing What Our Parser Accepts. It Was Wrong for 70 of 75 Attributes.

Most of the wrong menus were hiding valid operators, so nobody filed a bug. Instead of patching the heuristic, we made the engine's parser publish its own grammar through the config schema.

Thomas BerdyThomas Berdy