← Blog

The plan that costs the most could download the least

Backend 3 min read

While publishing something unrelated, a check against the live deployment returned this for a Max account:

hub       null    early   entitled=true   not_published
perch     null    early   entitled=true   not_published
argon     0.1.0   early   entitled=true
typeswap  1.0.0   early   entitled=true

entitled: true, and nothing to download. A Pro account, on the same deployment, at the same moment, could install both.

How#

Max gets builds first. That is what the tier is for, and it is implemented as a channel derived from the plan — never accepted from the client, because a client asking for early is asking to be given something it has not bought:

export function channelForPlan(plan) {
  return plan === 'max' ? 'early' : 'stable';
}

The release manifest is then read at manifests/<app>/<channel>.json, with no fallback. And nothing had ever been published to early, because staging a build is something you do when you have a build to stage. The publish script wrote stable and stopped, which is the correct behaviour for a release that is not staged.

So the lookup was a hit for everyone on stable and a miss for the one plan pinned to early. Not a permissions bug, not a billing bug — an absent file, in the one code path only the top tier takes.

Why nothing caught it#

The test suite publishes both channels in its setup, for a reason written in a comment right above it: the manifest reader keeps a module-global cache, so a test that publishes one channel late is served an earlier test's memory of "nothing published there". Publishing both in beforeEach fixed that, and in fixing it removed the only state in which the bug exists.

A fixture that makes the suite reliable had quietly made one production configuration unreachable. That is not an argument against the fixture. It is an argument for occasionally asking the deployment a question instead of asking the tests.

The fix, and the direction that matters#

early means "gets builds first". It does not mean "gets other builds". A Max customer is entitled to the stable build too — it is the same product, and when nothing has been staged it is the only build in existence.

So the resolution falls back:

async function resolveChannel(env, app, plan) {
  const wanted = channelForPlan(plan);
  const doc = await readManifest(env, app, wanted);
  if (doc || wanted === 'stable') return { channel: wanted, doc };
  return { channel: 'stable', doc: await readManifest(env, app, 'stable') };
}

The half that needed care is redemption. Downloads are minted as single-use tickets and redeemed a few minutes later, and that gap is re-checked — because a Max customer could otherwise mint an early-channel ticket, downgrade to Pro, and redeem it inside the window. The check used to be an equality test:

if (parts[2] !== channelForPlan(plan)) throw new ApiError(403, 'not_entitled');

Equality is what breaks under the fallback, since the ticket now legitimately names stable for an account whose channel is early. Replacing it with "which channels may this plan be served from" keeps the property intact and fixes the direction that was wrong:

export function channelAllowed(plan, channel) {
  switch (channel) {
    case 'stable': return true;
    case 'early':  return channelForPlan(plan) === 'early';
    default:       return false;
  }
}

Stable is not a privilege; early is. The default: false is not decoration — the value arrives out of a sealed key, and an unrecognised one should be refused rather than allowed by omission.

The part worth keeping#

Every plan resolved correctly by the rules. The rule said Max reads early, Max read early, and early was empty. Nobody wrote a check that a Max account can actually download the things it pays for, because that felt like testing that arithmetic works.

The three tests that exist now use a different app id than the rest of the file, specifically so the cache cannot hide the absence of a channel. They assert the boring thing: that the most expensive plan can install the product.